/*
 * 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
 *
 *   https://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.IOException;

import javax.imageio.stream.FileImageInputStream;

import org.apache.bcel.Const;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.Constant;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.util.BCELifier;

/**
 * Display Java .class file data. Output is based on javap tool. Built using the BCEL libary.
 */
final class ClassDumper {

    private final FileImageInputStream file;
    private final String fileName;
    private int superclassNameIndex;
    private int major;
    private int minor; // Compiler version
    private int accessFlags; // Access rights of parsed class
    private int[] interfaces; // Names of implemented interfaces
    private ConstantPool constantPool; // collection of constants
    private Constant[] constantItems; // collection of constants
    // private Field[] fields; // class fields, that is, its variables
    // private Method[] methods; // methods defined in the class
    private Attribute[] attributes; // attributes defined in the class

    /**
     * Parses class from the given stream.
     *
     * @param file Input stream.
     * @param fileName File name.
     */
    public ClassDumper(final FileImageInputStream file, final String fileName) {
        this.fileName = fileName;
        this.file = file;
    }

    private String constantToString(final int index) {
        final Constant c = constantItems[index];
        return constantPool.constantToString(c);
    }

    /**
     * Parses the given Java class file and return an object that represents the contained data, that is, constants, methods,
     * fields and commands. A <em>ClassFormatException</em> is raised, if the file is not a valid .class file. (This does
     * not include verification of the byte code as it is performed by the Java interpreter).
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    public void dump() throws IOException, ClassFormatException {
        // Check magic tag of class file
        processID();
        // Get compiler version
        processVersion();
        // process constant pool entries
        processConstantPool();
        // Get class information
        processClassInfo();
        // Get interface information, that is, implemented interfaces
        processInterfaces();
        // process class fields, that is, the variables of the class
        processFields();
        // process class methods, that is, the functions in the class
        processMethods();
        // process class attributes
        processAttributes();
    }

    /**
     * Processes information about the attributes of the class.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processAttributes() throws IOException, ClassFormatException {
        final int attributesCount = file.readUnsignedShort();
        attributes = new Attribute[attributesCount];

        System.out.printf("%nAttributes(%d):%n", attributesCount);

        for (int i = 0; i < attributesCount; i++) {
            attributes[i] = Attribute.readAttribute(file, constantPool);
            // indent all lines by two spaces
            final String[] lines = attributes[i].toString().split("\\r?\\n");
            for (final String line : lines) {
                System.out.println("  " + line);
            }
        }
    }

    /**
     * Processes information about the class and its super class.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processClassInfo() throws IOException, ClassFormatException {
        accessFlags = file.readUnsignedShort();
        /*
         * Interfaces are implicitly abstract, the flag should be set according to the JVM specification.
         */
        if ((accessFlags & Const.ACC_INTERFACE) != 0) {
            accessFlags |= Const.ACC_ABSTRACT;
        }
        if ((accessFlags & Const.ACC_ABSTRACT) != 0 && (accessFlags & Const.ACC_FINAL) != 0) {
            throw new ClassFormatException("Class " + fileName + " can't be both final and abstract");
        }

        System.out.printf("%nClass info:%n");
        System.out.println("  flags: " + BCELifier.printFlags(accessFlags, BCELifier.FLAGS.CLASS));
        final int classNameIndex = file.readUnsignedShort();
        System.out.printf("  this_class: %d (", classNameIndex);
        System.out.println(constantToString(classNameIndex) + ")");

        superclassNameIndex = file.readUnsignedShort();
        System.out.printf("  super_class: %d (", superclassNameIndex);
        if (superclassNameIndex > 0) {
            System.out.printf("%s", constantToString(superclassNameIndex));
        }
        System.out.println(")");
    }

    /**
     * Processes constant pool entries.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processConstantPool() throws IOException, ClassFormatException {
        byte tag;
        final int constantPoolCount = file.readUnsignedShort();
        constantItems = new Constant[constantPoolCount];
        constantPool = new ConstantPool(constantItems);

        // constantPool[0] is unused by the compiler
        System.out.printf("%nConstant pool(%d):%n", constantPoolCount - 1);

        for (int i = 1; i < constantPoolCount; i++) {
            constantItems[i] = Constant.readConstant(file);
            // i'm sure there is a better way to do this
            if (i < 10) {
                System.out.printf("    #%1d = ", i);
            } else if (i < 100) {
                System.out.printf("   #%2d = ", i);
            } else {
                System.out.printf("  #%d = ", i);
            }
            System.out.println(constantItems[i]);

            // All eight byte constants take up two spots in the constant pool
            tag = constantItems[i].getTag();
            if (tag == Const.CONSTANT_Double || tag == Const.CONSTANT_Long) {
                i++;
            }
        }
    }

    /**
     * Constructs object from file stream.
     *
     * @param file Input stream.
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processFieldOrMethod() throws IOException, ClassFormatException {
        final int accessFlags = file.readUnsignedShort();
        final int nameIndex = file.readUnsignedShort();
        System.out.printf("  nameIndex: %d (", nameIndex);
        System.out.println(constantToString(nameIndex) + ")");
        System.out.println("  accessFlags: " + BCELifier.printFlags(accessFlags, BCELifier.FLAGS.METHOD));
        final int descriptorIndex = file.readUnsignedShort();
        System.out.printf("  descriptorIndex: %d (", descriptorIndex);
        System.out.println(constantToString(descriptorIndex) + ")");

        final int attributesCount = file.readUnsignedShort();
        final Attribute[] attributes = new Attribute[attributesCount];
        System.out.println("  attribute count: " + attributesCount);

        for (int i = 0; i < attributesCount; i++) {
            // going to peek ahead a bit
            file.mark();
            final int attributeNameIndex = file.readUnsignedShort();
            final int attributeLength = file.readInt();
            // restore file location
            file.reset();
            // Usefull for debugging
            // System.out.printf(" attribute_name_index: %d (", attribute_name_index);
            // System.out.println(constantToString(attribute_name_index) + ")");
            // System.out.printf(" atribute offset in file: %x%n", + file.getStreamPosition());
            // System.out.println(" atribute_length: " + attribute_length);

            // A stronger verification test would be to read attribute_length bytes
            // into a buffer. Then pass that buffer to readAttribute and also
            // verify we're at EOF of the buffer on return.

            final long pos1 = file.getStreamPosition();
            attributes[i] = Attribute.readAttribute(file, constantPool);
            final long pos2 = file.getStreamPosition();
            if (pos2 - pos1 != attributeLength + 6) {
                System.out.printf("%nattributeLength: %d pos2-pos1-6: %d pos1: %x(%d) pos2: %x(%d)%n", attributeLength, pos2 - pos1 - 6, pos1, pos1, pos2,
                    pos2);
            }
            System.out.printf("  ");
            System.out.println(attributes[i]);
        }
    }

    /**
     * Processes information about the fields of the class, that is, its variables.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processFields() throws IOException, ClassFormatException {
        final int fieldsCount = file.readUnsignedShort();
        // fields = new Field[fieldsCount];

        // sometimes fields[0] is magic used for serialization
        System.out.printf("%nFields(%d):%n", fieldsCount);

        for (int i = 0; i < fieldsCount; i++) {
            processFieldOrMethod();
            if (i < fieldsCount - 1) {
                System.out.println();
            }
        }
    }

    /**
     * Checks whether the header of the file is ok. Of course, this has to be the first action on successive file reads.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processID() throws IOException, ClassFormatException {
        final int magic = file.readInt();
        if (magic != Const.JVM_CLASSFILE_MAGIC) {
            throw new ClassFormatException(fileName + " is not a Java .class file");
        }
        System.out.println("Java Class Dump");
        System.out.println("  file: " + fileName);
        System.out.printf("%nClass header:%n");
        System.out.printf("  magic: %X%n", magic);
    }

    /**
     * Processes information about the interfaces implemented by this class.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processInterfaces() throws IOException, ClassFormatException {
        final int interfacesCount = file.readUnsignedShort();
        interfaces = new int[interfacesCount];

        System.out.printf("%nInterfaces(%d):%n", interfacesCount);

        for (int i = 0; i < interfacesCount; i++) {
            interfaces[i] = file.readUnsignedShort();
            // i'm sure there is a better way to do this
            if (i < 10) {
                System.out.printf("   #%1d = ", i);
            } else if (i < 100) {
                System.out.printf("  #%2d = ", i);
            } else {
                System.out.printf(" #%d = ", i);
            }
            System.out.println(interfaces[i] + " (" + constantPool.getConstantString(interfaces[i], Const.CONSTANT_Class) + ")");
        }
    }

    /**
     * Processes information about the methods of the class.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processMethods() throws IOException, ClassFormatException {
        final int methodsCount = file.readUnsignedShort();
        // methods = new Method[methodsCount];

        System.out.printf("%nMethods(%d):%n", methodsCount);

        for (int i = 0; i < methodsCount; i++) {
            processFieldOrMethod();
            if (i < methodsCount - 1) {
                System.out.println();
            }
        }
    }

    /**
     * Processes major and minor version of compiler which created the file.
     *
     * @throws IOException if an I/O error occurs.
     * @throws ClassFormatException
     */
    private void processVersion() throws IOException, ClassFormatException {
        minor = file.readUnsignedShort();
        System.out.printf("  minor version: %s%n", minor);

        major = file.readUnsignedShort();
        System.out.printf("  major version: %s%n", major);
    }

}

final class DumpClass {

    public static void main(final String[] args) throws IOException {
        if (args.length != 1) {
            throw new IllegalArgumentException("Require file name as only argument");
        }
        try (FileImageInputStream file = new FileImageInputStream(new File(args[0]))) {
            new ClassDumper(file, args[0]).dump();
        }
        System.out.printf("End of Class Dump%n");
    }
}
