/*
 * Copyright (c) 2009, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package org.openjdk.asmtools.jdec;

import org.openjdk.asmtools.common.Module;
import org.openjdk.asmtools.asmutils.StringUtils;
import org.openjdk.asmtools.jasm.Modifiers;
import org.openjdk.asmtools.jcoder.JcodTokens;
import org.openjdk.asmtools.util.I18NResourceBundle;

import java.awt.event.KeyEvent;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.PrintWriter;

import static java.lang.String.format;
import static org.openjdk.asmtools.jasm.Tables.*;
import static org.openjdk.asmtools.jasm.Tables.AnnotElemType.AE_UNKNOWN;
import static org.openjdk.asmtools.jasm.TypeAnnotationTypes.*;

/**
 * Class data of the Java Decoder
 */
class ClassData {

    private byte[] types;
    private Object[] cpool;
    private int CPlen;
    private NestedByteArrayInputStream countedin;
    private DataInputStream in;
    private PrintWriter out;
    private int[] cpe_pos;
    private boolean printDetails;
    private String entityType = "";
    private String entityName = "";

    public static I18NResourceBundle i18n
            = I18NResourceBundle.getBundleForClass(Main.class);

    ClassData(DataInputStream dis, int printFlags, PrintWriter out) throws IOException {
        byte[] buf = new byte[dis.available()];
        try {
            if (dis.read(buf) <= 0)
                throw new IOException("The file is empty");
        } finally {
            dis.close();
        }
        countedin = new NestedByteArrayInputStream(buf);
        in = new DataInputStream(countedin);
        this.out = out;
        printDetails = ((printFlags & 1) == 1);
    }

    /*========================================================*/
    private static final char[] hexTable = {
            '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
    };

    private String toHex(long val, int width) {
        StringBuilder s = new StringBuilder();
        for (int i = width * 2 - 1; i >= 0; i--) {
            s.append(hexTable[((int) (val >> (4 * i))) & 0xF]);
        }
        return "0x" + s.toString();
    }

    private String toHex(long val) {
        int width;
        for (width = 8; width > 0; width--) {
            if ((val >> (width - 1) * 8) != 0) {
                break;
            }
        }
        return toHex(val, width);
    }

    private void printByteHex(PrintWriter out, int b) {
        out.print(hexTable[(b >> 4) & 0xF]);
        out.print(hexTable[b & 0xF]);
    }

    private void printBytes(PrintWriter out, DataInputStream in, int len)
            throws IOException {
        try {
            for (int i = 0; i < len; i++) {
                if (i % 8 == 0) {
                    out_print("0x");
                }
                printByteHex(out, in.readByte());
                if (i % 8 == 7) {
                    out.println(";");
                }
            }
        } finally {
            if (len % 8 != 0) {
                out.println(";");
            }
        }
    }

    private void printRestOfBytes() {
        for (int i = 0; ; i++) {
            try {
                byte b = in.readByte();
                if (i % 8 == 0) {
                    out_print("0x");
                }
                printByteHex(out, b);
                if (i % 8 == 7) {
                    out.print(";\n");
                }
            } catch (IOException e) {
                return;
            }
        }
    }

    private void printUtf8InfoIndex(int index, String indexName) {
        String name = (String) cpool[index];
        out_print("#" + index + "; // ");
        if (printDetails) {
            out.println(String.format("%-16s",indexName) + " : " + name);
        } else {
            out.println(indexName);
        }
    }

    /*========================================================*/
    private int shift = 0;

    private void out_begin(String s) {
        for (int i = 0; i < shift; i++) {
            out.print("  ");
        }
        out.println(s);
        shift++;
    }

    private void out_print(String s) {
        for (int i = 0; i < shift; i++) {
            out.print("  ");
        }
        out.print(s);
    }

    private void out_println(String s) {
        for (int i = 0; i < shift; i++) {
            out.print("  ");
        }
        out.println(s);
    }

    private void out_end(String s) {
        shift--;
        for (int i = 0; i < shift; i++) {
            out.print("  ");
        }
        out.println(s);
    }

    private String startArray(int length) {
        return "[" + (printDetails ? Integer.toString(length) : "") + "]";
    }

    private void startArrayCmt(int length, String comment) {
        out_begin(startArray(length) + format(" {%s", comment == null ? "" : " // " + comment));
    }

    private void startArrayCmtB(int length, String comment) {
        out_begin(startArray(length) + format("b {%s", comment == null ? "" : " // " + comment));
    }

    /*========================================================*/
    private void readCP(DataInputStream in) throws IOException {
        int length = in.readUnsignedShort();
        CPlen = length;
        traceln(i18n.getString("jdec.trace.CP_len", length));
        types = new byte[length];
        cpool = new Object[length];
        cpe_pos = new int[length];
        for (int i = 1; i < length; i++) {
            byte btag;
            int v1;
            long lv;
            cpe_pos[i] = countedin.getPos();
            btag = in.readByte();
            traceln(i18n.getString("jdec.trace.CP_entry", i, btag));
            types[i] = btag;
            ConstType tg = tag(btag);
            switch (tg) {
                case CONSTANT_UTF8:
                    cpool[i] = in.readUTF();
                    break;
                case CONSTANT_INTEGER:
                    v1 = in.readInt();
                    cpool[i] = v1;
                    break;
                case CONSTANT_FLOAT:
                    v1 = Float.floatToIntBits(in.readFloat());
                    cpool[i] = v1;
                    break;
                case CONSTANT_LONG:
                    lv = in.readLong();
                    cpool[i] = lv;
                    i++;
                    break;
                case CONSTANT_DOUBLE:
                    lv = Double.doubleToLongBits(in.readDouble());
                    cpool[i] = lv;
                    i++;
                    break;
                case CONSTANT_CLASS:
                case CONSTANT_STRING:
                case CONSTANT_MODULE:
                case CONSTANT_PACKAGE:
                    v1 = in.readUnsignedShort();
                    cpool[i] = v1;
                    break;
                case CONSTANT_INTERFACEMETHOD:
                case CONSTANT_FIELD:
                case CONSTANT_METHOD:
                case CONSTANT_NAMEANDTYPE:
                    cpool[i] = "#" + in.readUnsignedShort() + " #" + in.readUnsignedShort();
                    break;
                case CONSTANT_DYNAMIC:
                case CONSTANT_INVOKEDYNAMIC:
                    cpool[i] = in.readUnsignedShort() + "s #" + in.readUnsignedShort();
                    break;
                case CONSTANT_METHODHANDLE:
                    cpool[i] = in.readUnsignedByte() + "b #" + in.readUnsignedShort();
                    break;
                case CONSTANT_METHODTYPE:
                    cpool[i] = "#" + in.readUnsignedShort();
                    break;
                default:
                    CPlen = i;
                    printCP(out);
                    out_println(toHex(btag, 1) + "; // invalid constant type: " + (int) btag + " for element " + i);
                    throw new ClassFormatError();
            }
        }
    }

    private void printCP(PrintWriter out) {
        int length = CPlen;
        startArrayCmt(length, "Constant Pool");
        out_println("; // first element is empty");
        try {
            int size;
            for (int i = 1; i < length; i = i + size) {
                size = 1;
                byte btag = types[i];
                ConstType tg = tag(btag);
                int pos = cpe_pos[i];
                String tagstr;
                String valstr;
                int v1;
                long lv;
                if (tg != null) {
                    tagstr = tg.parseKey();
                } else {
                    throw new Error("Can't get a tg representing the type of Constant in the Constant Pool at: " + i);
                }
                switch (tg) {
                    case CONSTANT_UTF8: {
                        tagstr = "Utf8";
                        valstr = StringUtils.Utf8ToString((String) cpool[i]);
                    }
                    break;
                    case CONSTANT_FLOAT:
                    case CONSTANT_INTEGER:
                        v1 = (Integer) cpool[i];
                        valstr = toHex(v1, 4);
                        break;
                    case CONSTANT_DOUBLE:
                    case CONSTANT_LONG:
                        lv = (Long) cpool[i];
                        valstr = toHex(lv, 8) + ";";
                        size = 2;
                        break;
                    case CONSTANT_CLASS:
                    case CONSTANT_MODULE:
                    case CONSTANT_PACKAGE:
                    case CONSTANT_STRING:
                        v1 = (Integer) cpool[i];
                        valstr = "#" + v1;
                        break;
                    case CONSTANT_INTERFACEMETHOD:
                    case CONSTANT_FIELD:
                    case CONSTANT_METHOD:
                    case CONSTANT_NAMEANDTYPE:
                    case CONSTANT_METHODHANDLE:
                    case CONSTANT_METHODTYPE:
                    case CONSTANT_DYNAMIC:
                    case CONSTANT_INVOKEDYNAMIC:
                        valstr = (String) cpool[i];
                        break;
                    default:
                        throw new Error("invalid constant type: " + (int) btag);
                }
                out_print(tagstr + " " + valstr + "; // #" + i);
                if (printDetails) {
                    out_println(" at " + toHex(pos));
                } else {
                    out.println();
                }
            }
        } finally {
            out_end("} // Constant Pool");
            out.println();
        }
    }

    private String getStringPos() {
        return " at " + toHex(countedin.getPos());
    }

    private String getCommentPosCond() {
        if (printDetails) {
            return " // " + getStringPos();
        } else {
            return "";
        }
    }

    private void decodeCPXAttr(DataInputStream in, int len, String attrname, PrintWriter out) throws IOException {
        decodeCPXAttrM(in, len, attrname, out, 1);
    }

    private void decodeCPXAttrM(DataInputStream in, int len, String attrname, PrintWriter out, int expectedIndices) throws IOException {
        if (len != expectedIndices * 2) {
            out_println("// invalid length of " + attrname + " attr: " + len + " (should be " + (expectedIndices * 2) + ") > ");
            printBytes(out, in, len);
        } else {
            StringBuilder outputString = new StringBuilder();
            for (int k = 1; k <= expectedIndices; k++) {
                outputString.append("#").append(in.readUnsignedShort()).append("; ");
                if (k % 16 == 0) {
                    out_println(outputString.toString().replaceAll("\\s+$",""));
                    outputString = new StringBuilder();
                }
            }
            if (outputString.length() > 0) {
                out_println(outputString.toString().replaceAll("\\s+$",""));
            }
        }
    }

    private void printStackMap(DataInputStream in, int elementsNum) throws IOException {
        int num;
        if (elementsNum > 0) {
            num = elementsNum;
        } else {
            num = in.readUnsignedShort();
        }
        out.print(startArray(num) + (elementsNum > 0 ? "z" : "") + "{");
        try {
            for (int k = 0; k < num; k++) {
                int maptype = in.readUnsignedByte();
                StackMapType mptyp = stackMapType(maptype, out);
                String maptypeImg;
                if (printDetails) {
                    maptypeImg = maptype + "b";
                } else {
                    try {
                        maptypeImg = mptyp.parsekey();
                    } catch (ArrayIndexOutOfBoundsException e) {
                        maptypeImg = "/* BAD TYPE: */ " + maptype + "b";
                    }
                }
                switch (mptyp) {
                    case ITEM_Object:
                    case ITEM_NewObject:
                        maptypeImg = maptypeImg + "," + in.readUnsignedShort();
                        break;
                    case ITEM_UNKNOWN:
                        maptypeImg = maptype + "b";
                        break;
                    default:
                }
                out.print(maptypeImg);
                if (k < num - 1) {
                    out.print("; ");
                }
            }
        } finally {
            out.print("}");
        }
    }

    /**
     * Processes 4.7.20 The RuntimeVisibleTypeAnnotations Attribute, 4.7.21 The RuntimeInvisibleTypeAnnotations Attribute
     * <code>type_annotation</code> structure.
     */
    private void decodeTargetTypeAndRefInfo(DataInputStream in) throws IOException {
        int tt = in.readUnsignedByte(); // [4.7.20] annotations[], type_annotation { u1 target_type; ...}
        ETargetType targetType = ETargetType.getTargetType(tt);
        if( targetType == null ) {
            throw new Error("Type annotation: invalid target_type(u1) " + tt);
        }
        ETargetInfo targetInfo = targetType.targetInfo();
        out_println(toHex(tt, 1) + ";  //  target_type: " + targetType.parseKey());
        switch (targetInfo) {
            case TYPEPARAM:          //[3.3.1] meth_type_param, class_type_param:
                out_println(toHex(in.readUnsignedByte(), 1) + ";  //  param_index");
                break;
            case SUPERTYPE:         //[3.3.2]  class_exts_impls
                out_println(toHex(in.readUnsignedShort(), 2) + ";  //  type_index");
                break;
            case TYPEPARAM_BOUND:   //[3.3.3]  class_type_param_bnds, meth_type_param_bnds
                out_println(toHex(in.readUnsignedByte(), 1) + ";  //  param_index");
                out_println(toHex(in.readUnsignedByte(), 1) + ";  //  bound_index");
                break;
            case EMPTY:             //[3.3.4]  meth_receiver, meth_ret_type, field
                // NOTE: reference_info is empty for this annotation's target
                break;
            case METHODPARAM:       //[3.3.5]  meth_formal_param:
                out_println(toHex(in.readUnsignedByte(), 1) + ";  //  parameter_index");
                break;
            case EXCEPTION:         //[3.3.61]  throws_type
                //KTL:  Updated index to UShort for JSR308 change
                out_println(in.readUnsignedShort() + ";  //  type_index");
                break;
            case LOCALVAR: //[3.3.7]  local_var, resource_var
            {
                int lv_num = in.readUnsignedShort();
                startArrayCmt(lv_num, "local_variables");
                try {
                    for (int i = 0; i < lv_num; i++) {
                        out_println(in.readUnsignedShort() + " " + in.readUnsignedShort()
                                + " " + in.readUnsignedShort() + ";" + getCommentPosCond());
                    }
                } finally {
                    out_end("}");
                }
            }
            break;
            case CATCH:             //[3.3.8]  exception_param
                out_println(in.readUnsignedShort() + ";  //  exception_table_index");
                break;
            case OFFSET:            //[3.3.9]  type_test (instanceof), obj_creat (new)
                // constr_ref_receiver, meth_ref_receiver
                out_println(in.readUnsignedShort() + ";  //  offset");
                break;
            case TYPEARG:           //[3.3.10]  cast, constr_ref_typearg, meth_invoc_typearg
                // constr_invoc_typearg, meth_ref_typearg
                out_println(in.readUnsignedShort() + ";  //  offset");
                out_println(toHex(in.readUnsignedByte(), 1) + ";  //  type_index");
                break;
            default:                // should never happen
                out_println(toHex(tt, 1) + "; // invalid target_info: " + tt);
                throw new ClassFormatError();
        }
        // [4.7.20.2]
        int path_length = in.readUnsignedByte();  // type_path { u1 path_length; ...}
        startArrayCmtB(path_length, "type_paths");
        try {
            for (int i = 0; i < path_length; i++) {
                // print the type_path elements
                out_println("{ " + toHex(in.readUnsignedByte(), 1)  // { u1 type_path_kind;
                        + "; " + toHex(in.readUnsignedByte(), 1)    //   u1 type_argument_index; }
                        + "; } // type_path[" + i + "]");           // path[i]
            }
        } finally {
            out_end("}");
        }
    }

    private void decodeElementValue(DataInputStream in, PrintWriter out) throws IOException {
        out_begin("{  //  element_value");
        try {
            char tg = (char) in.readByte();
            AnnotElemType tag = annotElemType(tg);
            if (tag != AE_UNKNOWN) {
                out_println("'" + tg + "';");
            }
            switch (tag) {
                case AE_BYTE:
                case AE_CHAR:
                case AE_DOUBLE:
                case AE_FLOAT:
                case AE_INT:
                case AE_LONG:
                case AE_SHORT:
                case AE_BOOLEAN:
                case AE_STRING:
                    decodeCPXAttr(in, 2, "const_value_index", out);
                    break;
                case AE_ENUM:
                    out_begin("{  //  enum_const_value");
                    decodeCPXAttr(in, 2, "type_name_index", out);
                    decodeCPXAttr(in, 2, "const_name_index", out);
                    out_end("}  //  enum_const_value");
                    break;
                case AE_CLASS:
                    decodeCPXAttr(in, 2, "class_info_index", out);
                    break;
                case AE_ANNOTATION:
                    decodeAnnotation(in, out);
                    break;
                case AE_ARRAY:
                    int ev_num = in.readUnsignedShort();
                    startArrayCmt(ev_num, "array_value");
                    try {
                        for (int i = 0; i < ev_num; i++) {
                            decodeElementValue(in, out);
                            if (i < ev_num - 1) {
                                out_println(";");
                            }
                        }
                    } finally {
                        out_end("}  //  array_value");
                    }
                    break;
                case AE_UNKNOWN:
                default:
                    String msg = "invalid element_value" + (isPrintableChar(tg) ? " tag type : " + tg : "");
                    out_println(toHex(tg, 1) + "; // " + msg);
                    throw new ClassFormatError(msg);
            }
        } finally {
            out_end("}  //  element_value");
        }
    }

    public boolean isPrintableChar(char c) {
        Character.UnicodeBlock block = Character.UnicodeBlock.of(c);
        return (!Character.isISOControl(c)) &&
                c != KeyEvent.CHAR_UNDEFINED &&
                block != null &&
                block != Character.UnicodeBlock.SPECIALS;
    }

    private void decodeAnnotation(DataInputStream in, PrintWriter out) throws IOException {
        out_begin("{  //  annotation");
        try {
            decodeCPXAttr(in, 2, "field descriptor", out);
            int evp_num = in.readUnsignedShort();
            decodeElementValuePairs(evp_num, in, out);
        } finally {
            out_end("}  //  annotation");
        }
    }

    private void decodeElementValuePairs(int count, DataInputStream in, PrintWriter out) throws IOException {
        startArrayCmt(count, "element_value_pairs");
        try {
            for (int i = 0; i < count; i++) {
                out_begin("{  //  element value pair");
                try {
                    decodeCPXAttr(in, 2, "name of the annotation type element", out);
                    decodeElementValue(in, out);
                } finally {
                    out_end("}  //  element value pair");
                    if (i < count - 1) {
                        out_println(";");
                    }
                }
            }
        } finally {
            out_end("}  //  element_value_pairs");
        }
    }

    /**
     * component_info {     JEP 359 Record(Preview): class file 58.65535
     *     u2               name_index;
     *     u2               descriptor_index;
     *     u2               attributes_count;
     *     attribute_info attributes[attributes_count];
     * }
     *
     * or
     * field_info {
     *     u2             access_flags;
     *     u2             name_index;
     *     u2             descriptor_index;
     *     u2             attributes_count;
     *     attribute_info attributes[attributes_count];
     * }
     * or
     * method_info {
     *     u2             access_flags;
     *     u2             name_index;
     *     u2             descriptor_index;
     *     u2             attributes_count;
     *     attribute_info attributes[attributes_count];
     * }
     *
     */
    private void decodeInfo(DataInputStream in, PrintWriter out, String elementName, boolean hasAccessFlag) throws IOException {
        out_begin("{  // " + elementName + (printDetails ? getStringPos() : ""));
        try {
            if(hasAccessFlag) {
                //  u2 access_flags;
                out_println(toHex(in.readShort(), 2) + "; // access");
            }
            // u2 name_index
            printUtf8InfoIndex(in.readUnsignedShort(), "name_index");
            // u2 descriptor_index
            printUtf8InfoIndex(in.readUnsignedShort(), "descriptor_index");
            // u2 attributes_count;
            // attribute_info attributes[attributes_count]
            decodeAttrs(in, out);
        } finally {
            out_end("}");
        }
    }

    private void decodeTypeAnnotation(DataInputStream in, PrintWriter out) throws IOException {
        out_begin("{  //  type_annotation");
        try {
            decodeTargetTypeAndRefInfo(in);
            decodeCPXAttr(in, 2, "field descriptor", out);
            int evp_num = in.readUnsignedShort();
            decodeElementValuePairs(evp_num, in, out);
        } finally {
            out_end("}  //  type_annotation");
        }
    }

    private void decodeBootstrapMethod(DataInputStream in) throws IOException {
        out_begin("{  //  bootstrap_method");
        try {
            out_println("#" + in.readUnsignedShort() + "; // bootstrap_method_ref");
            int bm_args_cnt = in.readUnsignedShort();
            startArrayCmt(bm_args_cnt, "bootstrap_arguments");
            try {
                for (int i = 0; i < bm_args_cnt; i++) {
                    out_println("#" + in.readUnsignedShort() + ";" + getCommentPosCond());
                }
            } finally {
                out_end("}  //  bootstrap_arguments");
            }
        } finally {
            out_end("}  //  bootstrap_method");
        }
    }

    private void decodeAttr(DataInputStream in, PrintWriter out) throws IOException {
        // Read one attribute
        String posComment = getStringPos();
        int name_cpx = in.readUnsignedShort(), btag, len;

        String AttrName = "";
        try {
            btag = types[name_cpx];
            ConstType tag = tag(btag);

            if (tag == ConstType.CONSTANT_UTF8) {
                AttrName = (String) cpool[name_cpx];
            }
        } catch (ArrayIndexOutOfBoundsException ignored) {
        }
        AttrTag tg = attrtag(AttrName);
        String endingComment = AttrName;
        len = in.readInt();
        countedin.enter(len);
        try {
            if (printDetails) {
                out_begin("Attr(#" + name_cpx + ", " + len + ") { // " + AttrName + posComment);
            } else {
                out_begin("Attr(#" + name_cpx + ") { // " + AttrName);
            }

            switch (tg) {
                case ATT_Code:
                    out_println(in.readUnsignedShort() + "; // max_stack");
                    out_println(in.readUnsignedShort() + "; // max_locals");
                    int code_len = in.readInt();
                    out_begin("Bytes" + startArray(code_len) + "{");
                    try {
                        printBytes(out, in, code_len);
                    } finally {
                        out_end("}");
                    }
                    int trap_num = in.readUnsignedShort();
                    startArrayCmt(trap_num, "Traps");
                    try {
                        for (int i = 0; i < trap_num; i++) {
                            out_println(in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + ";" +
                                    getCommentPosCond());
                        }
                    } finally {
                        out_end("} // end Traps");
                    }
                    // Read the attributes
                    decodeAttrs(in, out);
                    break;

                case ATT_Exceptions:
                    int count = in.readUnsignedShort();
                    startArrayCmt(count, AttrName);
                    try {
                        for (int i = 0; i < count; i++) {
                            out_println("#" + in.readUnsignedShort() + ";" +
                                    getCommentPosCond());
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_LineNumberTable:
                    int ll_num = in.readUnsignedShort();
                    startArrayCmt(ll_num, "line_number_table");
                    try {
                        for (int i = 0; i < ll_num; i++) {
                            out_println(in.readUnsignedShort() + "  " +
                                    in.readUnsignedShort() + ";" +
                                    getCommentPosCond());
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_LocalVariableTable:
                case ATT_LocalVariableTypeTable:
                    int lvt_num = in.readUnsignedShort();
                    startArrayCmt(lvt_num, AttrName);
                    try {
                        for (int i = 0; i < lvt_num; i++) {
                            out_println(in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + ";" +
                                    getCommentPosCond());
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_InnerClasses:
                    int ic_num = in.readUnsignedShort();
                    startArrayCmt(ic_num, "classes");
                    try {
                        for (int i = 0; i < ic_num; i++) {
                            out_println("#" + in.readUnsignedShort() + " #" +
                                    in.readUnsignedShort() + " #" +
                                    in.readUnsignedShort() + " " +
                                    in.readUnsignedShort() + ";" + getCommentPosCond());
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_StackMap:
                    int e_num = in.readUnsignedShort();
                    startArrayCmt(e_num, "");
                    try {
                        for (int k = 0; k < e_num; k++) {
                            int start_pc = in.readUnsignedShort();
                            out_print("" + start_pc + ", ");
                            printStackMap(in, 0);
                            out.print(", ");
                            printStackMap(in, 0);
                            out.println(";");
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_StackMapTable:
                    int et_num = in.readUnsignedShort();
                    startArrayCmt(et_num, "");
                    try {
                        for (int k = 0; k < et_num; k++) {
                            int frame_type = in.readUnsignedByte();
                            StackMapFrameType ftype = stackMapFrameType(frame_type);
                            switch (ftype) {
                                case SAME_FRAME:
                                    // type is same_frame;
                                    out_print("" + frame_type + "b");
                                    out.println("; // same_frame");
                                    break;
                                case SAME_LOCALS_1_STACK_ITEM_FRAME:
                                    // type is same_locals_1_stack_item_frame
                                    out_print("" + frame_type + "b, ");
                                    // read additional single stack element
                                    printStackMap(in, 1);
                                    out.println("; // same_locals_1_stack_item_frame");
                                    break;
                                case SAME_LOCALS_1_STACK_ITEM_EXTENDED_FRAME:
                                    // type is same_locals_1_stack_item_frame_extended
                                    int noffset = in.readUnsignedShort();
                                    out_print("" + frame_type + "b, " + noffset + ", ");
                                    // read additional single stack element
                                    printStackMap(in, 1);
                                    out.println("; // same_locals_1_stack_item_frame_extended");
                                    break;
                                case CHOP_1_FRAME:
                                case CHOP_2_FRAME:
                                case CHOP_3_FRAME:
                                    // type is chop_frame
                                    int coffset = in.readUnsignedShort();
                                    out_print("" + frame_type + "b, " + coffset);
                                    out.println("; // chop_frame " + (251 - frame_type));
                                    break;
                                case SAME_FRAME_EX:
                                    // type is same_frame_extended;
                                    int xoffset = in.readUnsignedShort();
                                    out_print("" + frame_type + "b, " + xoffset);
                                    out.println("; // same_frame_extended");
                                    break;
                                case APPEND_FRAME:
                                    // type is append_frame
                                    int aoffset = in.readUnsignedShort();
                                    out_print("" + frame_type + "b, " + aoffset + ", ");
                                    // read additional locals
                                    printStackMap(in, frame_type - 251);
                                    out.println("; // append_frame " + (frame_type - 251));
                                    break;
                                case FULL_FRAME:
                                    // type is full_frame
                                    int foffset = in.readUnsignedShort();
                                    out_print("" + frame_type + "b, " + foffset + ", ");
                                    printStackMap(in, 0);
                                    out.print(", ");
                                    printStackMap(in, 0);
                                    out.println("; // full_frame");
                                    break;
                            }
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_EnclosingMethod:
                    decodeCPXAttrM(in, len, AttrName, out, 2);
                    break;
                case ATT_AnnotationDefault:
                    decodeElementValue(in, out);
                    break;
                case ATT_RuntimeInvisibleAnnotations:
                case ATT_RuntimeVisibleAnnotations:
                    int an_num = in.readUnsignedShort();
                    startArrayCmt(an_num, "annotations");
                    try {
                        for (int i = 0; i < an_num; i++) {
                            decodeAnnotation(in, out);
                            if (i < an_num - 1) {
                                out_println(";");
                            }
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                // 4.7.20 The RuntimeVisibleTypeAnnotations Attribute
                // 4.7.21 The RuntimeInvisibleTypeAnnotations Attribute
                case ATT_RuntimeInvisibleTypeAnnotations:
                case ATT_RuntimeVisibleTypeAnnotations:
                    int ant_num = in.readUnsignedShort();
                    startArrayCmt(ant_num, "annotations");
                    try {
                        for (int i = 0; i < ant_num; i++) {
                            decodeTypeAnnotation(in, out);
                            if (i < ant_num - 1) {
                                out_println(";");
                            }
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_RuntimeInvisibleParameterAnnotations:
                case ATT_RuntimeVisibleParameterAnnotations:
                    int pm_num = in.readUnsignedByte();
                    startArrayCmtB(pm_num, "parameters");
                    try {
                        for (int k = 0; k < pm_num; k++) {
                            int anp_num = in.readUnsignedShort();
                            startArrayCmt(anp_num, "annotations");
                            try {
                                for (int i = 0; i < anp_num; i++) {
                                    decodeAnnotation(in, out);
                                    if (k < anp_num - 1) {
                                        out_println(";");
                                    }
                                }
                            } finally {
                                out_end("}");
                            }
                            if (k < pm_num - 1) {
                                out_println(";");
                            }
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_BootstrapMethods:
                    int bm_num = in.readUnsignedShort();
                    startArrayCmt(bm_num, "bootstrap_methods");
                    try {
                        for (int i = 0; i < bm_num; i++) {
                            decodeBootstrapMethod(in);
                            if (i < bm_num - 1) {
                                out_println(";");
                            }
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_Module:
                    decodeModule(in);
                    break;
                case ATT_TargetPlatform:
                    decodeCPXAttrM(in, len, AttrName, out, 3);
                    break;
                case ATT_ModulePackages:
                    int p_num = in.readUnsignedShort();
                    startArrayCmt(p_num, null);
                    try {
                        decodeCPXAttrM(in, len - 2, AttrName, out, p_num);
                    } finally {
                        out_end("}");
                    }
                    break;
                //  MethodParameters_attribute {
                //    u2 attribute_name_index;
                //    u4 attribute_length;
                //    u1 parameters_count;
                //    {   u2 name_index;
                //        u2 access_flags;
                //    } parameters[parameters_count];
                //  }
                case ATT_MethodParameters:
                    int pcount = in.readUnsignedByte();
                    startArrayCmtB(pcount, AttrName);
                    try {
                        for (int i = 0; i < pcount; i++) {
                            out_println("#" + in.readUnsignedShort() + "  " +
                                    toHex(in.readUnsignedShort(), 2) + ";" +
                                    getCommentPosCond());
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                //  JEP 359 Record(Preview): class file 58.65535
                //  Record_attribute {
                //      u2 attribute_name_index;
                //      u4 attribute_length;
                //      u2 components_count;
                //      component_info components[components_count];
                //  }
                case ATT_Record:
                    int ncomps = in.readUnsignedShort();
                    startArrayCmt(ncomps, "components");
                    try {
                        for (int i = 0; i < ncomps; i++) {
                            decodeInfo(in,out,"component",false);
                            if (i < ncomps - 1) {
                                out_println(";");
                            }
                        }
                    } finally {
                        out_end("}");
                    }
                    break;
                case ATT_ConstantValue:
                case ATT_Signature:
                case ATT_SourceFile:
                    decodeCPXAttr(in, len, AttrName, out);
                    break;
                    //  JEP 181 (Nest-based Access Control): class file 55.0
                    //  NestHost_attribute {
                    //    u2 attribute_name_index;
                    //    u4 attribute_length;
                    //    u2 host_class_index;
                    //  }
                case ATT_NestHost:
                    decodeTypes(in, out, 1);
                    break;
                    //  JEP 181 (Nest-based Access Control): class file 55.0
                    //  NestMembers_attribute {
                    //    u2 attribute_name_index;
                    //    u4 attribute_length;
                    //    u2 number_of_classes;
                    //    u2 classes[number_of_classes];
                    //  }
                case ATT_NestMembers:
                    //  JEP 360 (Sealed types): class file 59.65535
                    //  PermittedSubclasses_attribute {
                    //    u2 attribute_name_index;
                    //    u4 attribute_length;
                    //    u2 number_of_classes;
                    //    u2 classes[number_of_classes];
                    //  }
                case ATT_PermittedSubclasses:
                    int nsubtypes = in.readUnsignedShort();
                    startArrayCmt(nsubtypes, "classes");
                    try {
                        decodeTypes(in, out, nsubtypes);
                    } finally {
                        out_end("}");
                    }
                    break;
                default:
                    printBytes(out, in, len);
                    if (AttrName == null) {
                        endingComment = "Attr(#" + name_cpx + ")";
                    }
            }

        } catch (EOFException e) {
            out.println("// ======== unexpected end of attribute array");
        } finally {
            int rest = countedin.available();
            if (rest > 0) {
                out.println("// ======== attribute array started " + posComment + " has " + rest + " bytes more:");
                printBytes(out, in, rest);
            }
            out_end("} // end " + endingComment);
            countedin.leave();
        }
    }

    private void decodeModuleStatement(String statementName, DataInputStream in) throws IOException {
        // u2 {exports|opens}_count
        int count = in.readUnsignedShort();
        startArrayCmt(count, statementName);
        try {
            for (int i = 0; i < count; i++) {
                // u2 {exports|opens}_index; u2 {exports|opens}_flags
                int index = in.readUnsignedShort();
                int nFlags = in.readUnsignedShort();
                String sFlags = printDetails ? Module.Modifier.getStatementFlags(nFlags) : "";
                out_println("#" + index + " " + toHex(nFlags, 2) + (sFlags.isEmpty() ? "" : " // [ " + sFlags + " ]"));
                int exports_to_count = in.readUnsignedShort();
                startArrayCmt(exports_to_count, null);
                try {
                    for (int j = 0; j < exports_to_count; j++) {
                        out_println("#" + in.readUnsignedShort() + ";");
                    }
                } finally {
                    out_end("};");
                }
            }
        } finally {
            out_end("} // " + statementName + "\n");
        }
    }

    private void decodeModule(DataInputStream in) throws IOException {
        //u2 module_name_index
        int index = in.readUnsignedShort();
        entityName = (String) cpool[(Integer) cpool[index]];
        out_print("#" + index + "; // ");
        if (printDetails) {
            out.println(String.format("%-16s","name_index") + " : " + entityName);
        } else {
            out.println("name_index");
        }

        // u2 module_flags
        int moduleFlags = in.readUnsignedShort();
        out_print(toHex(moduleFlags, 2) + "; // flags");
        if (printDetails) {
            out_print(" " + Module.Modifier.getModuleFlags(moduleFlags));
        }
        out.println();

        //u2 module_version
        int versionIndex = in.readUnsignedShort();
        out_println("#" + versionIndex + "; // version");

        // u2 requires_count
        int count = in.readUnsignedShort();
        startArrayCmt(count, "requires");
        try {
            for (int i = 0; i < count; i++) {
                // u2 requires_index; u2 requires_flags; u2 requires_version_index
                index = in.readUnsignedShort();
                int nFlags = in.readUnsignedShort();
                versionIndex = in.readUnsignedShort();
                String sFlags = printDetails ? Module.Modifier.getStatementFlags(nFlags) : "";
                out_println("#" + index + " " + toHex(nFlags, 2) + " #" + versionIndex + ";" + (sFlags.isEmpty() ? "" : " // " + sFlags));
            }
        } finally {
            out_end("} // requires\n");
        }

        decodeModuleStatement("exports", in);

        decodeModuleStatement("opens", in);
        // u2 uses_count
        count = in.readUnsignedShort();
        startArrayCmt(count, "uses");
        try {
            for (int i = 0; i < count; i++) {
                // u2 uses_index
                out_println("#" + in.readUnsignedShort() + ";");
            }
        } finally {
            out_end("} // uses\n");
        }
        count = in.readUnsignedShort(); // u2 provides_count
        startArrayCmt(count, "provides");
        try {
            for (int i = 0; i < count; i++) {
                // u2 provides_index
                out_println("#" + in.readUnsignedShort());
                int provides_with_count = in.readUnsignedShort();
                // u2 provides_with_count
                startArrayCmt(provides_with_count, null);
                try {
                    for (int j = 0; j < provides_with_count; j++) {
                        // u2 provides_with_index;
                        out_println("#" + in.readUnsignedShort() + ";");
                    }
                } finally {
                    out_end("};");
                }
            }
        } finally {
            out_end("} // provides\n");
        }
    }

    private void decodeAttrs(DataInputStream in, PrintWriter out) throws IOException {
        // Read the attributes
        int attr_num = in.readUnsignedShort();
        startArrayCmt(attr_num, "Attributes");
        try {
            for (int i = 0; i < attr_num; i++) {
                decodeAttr(in, out);
                if (i + 1 < attr_num) {
                    out_println(";");
                }
            }
        } finally {
            out_end("} // Attributes");
        }
    }

    private void decodeMembers(DataInputStream in, PrintWriter out, String groupName, String elementName) throws IOException {
        int count = in.readUnsignedShort();
        traceln(groupName + "=" + count);
        startArrayCmt(count, groupName);
        try {
            for (int i = 0; i < count; i++) {
                decodeInfo(in,out,elementName,true);
                if (i + 1 < count) {
                    out_println(";");
                }
            }
        } finally {
            out_end("} // " + groupName);
            out.println();
        }
    }

    void decodeClass(String fileName) throws IOException {
        // Read the header
        try {
            int magic = in.readInt();
            int min_version = in.readUnsignedShort();
            int version = in.readUnsignedShort();

            // Read the constant pool
            readCP(in);
            short access = in.readShort(); // don't care about sign
            int this_cpx = in.readUnsignedShort();

            try {
                entityName = (String) cpool[(Integer) cpool[this_cpx]];
                if (entityName.equals("module-info")) {
                    entityType = "module";
                    entityName = "";
                } else {
                    entityType = "class";
                }
                if (!entityName.isEmpty() && (JcodTokens.keyword_token_ident(entityName) != JcodTokens.Token.IDENT || JcodTokens.constValue(entityName) != -1)) {
                    // JCod can't parse a entityName matching a keyword or a constant value,
                    // then use the filename instead:
                    out_begin(String.format("file \"%s.class\" {", entityName));
                } else {
                    out_begin(format("%s %s {", entityType, entityName));
                }
            } catch (Exception e) {
                entityName = fileName;
                out.println("// " + e.getMessage() + " while accessing entityName");
                out_begin(format("%s %s { // source file name", entityType, entityName));
            }

            out_print(toHex(magic, 4) + ";");
            if (magic != JAVA_MAGIC) {
                out.print(" // wrong magic: 0x" + Integer.toString(JAVA_MAGIC, 16) + " expected");
            }
            out.println();
            out_println(min_version + "; // minor version");
            out_println(version + "; // version");

            // Print the constant pool
            printCP(out);
            out_println(toHex(access, 2) + "; // access" +
                    (printDetails ? " [" + (" " + Modifiers.accessString(access, CF_Context.CTX_CLASS).toUpperCase()).replaceAll(" (\\S)", " ACC_$1") + "]" : ""));
            out_println("#" + this_cpx + ";// this_cpx");
            int super_cpx = in.readUnsignedShort();
            out_println("#" + super_cpx + ";// super_cpx");
            traceln(i18n.getString("jdec.trace.access_thisCpx_superCpx", access, this_cpx, super_cpx));
            out.println();

            // Read the interfaces
            int numinterfaces = in.readUnsignedShort();
            traceln(i18n.getString("jdec.trace.numinterfaces", numinterfaces));
            startArrayCmt(numinterfaces, "Interfaces");
            try {
                decodeTypes(in, out, numinterfaces);
            } finally {
                out_end("} // Interfaces\n");
            }
            // Read the fields
            decodeMembers(in, out, "Fields", "field");

            // Read the methods
            decodeMembers(in, out, "Methods", "method");

            // Read the attributes
            decodeAttrs(in, out);
        } catch (EOFException ignored) {
        } catch (ClassFormatError err) {
            String msg = err.getMessage();
            out.println("//------- ClassFormatError" +
                    (msg == null || msg.isEmpty() ? "" : ": " + msg));
            printRestOfBytes();
        } finally {
            out_end(format("} // end %s %s", entityType, entityName));
        }
    } // end decodeClass()

    private void decodeTypes(DataInputStream in, PrintWriter out, int count) throws IOException {
        for (int i = 0; i < count; i++) {
            int type_cpx = in.readUnsignedShort();
            traceln(i18n.getString("jdec.trace.type", i, type_cpx));
            out_print("#" + type_cpx + ";");
            if (printDetails) {
                String name = (String) cpool[(int)cpool[type_cpx]];
                out.println(" // " + name + getStringPos());
            } else {
                out.println();
            }
        }
    }

    /* ====================================================== */
    boolean DebugFlag = false;

    public void trace(String s) {
        if (!DebugFlag) {
            return;
        }
        System.out.print(s);
    }

    public void traceln(String s) {
        if (!DebugFlag) {
            return;
        }
        System.out.println(s);
    }
}// end class ClassData

