/*
 * Copyright (c) 1996, 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.jasm;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

import static org.openjdk.asmtools.jasm.Tables.*;

/**
 * ClassData
 *
 * This is the main data structure for representing parsed class data. This structure
 * renders directly to a class file.
 *
 */
class ClassData extends MemberData {

    /* ClassData Fields */
    CFVersion cfv;
    ConstantPool.ConstCell me, father;
    String myClassName;
    AttrData sourceFileNameAttr;
    ArrayList<Argument> interfaces;
    ArrayList<FieldData> fields = new ArrayList<>();
    ArrayList<MethodData> methods = new ArrayList<>();
    DataVectorAttr<InnerClassData> innerClasses = null;
    DataVectorAttr<BootstrapMethodData> bootstrapMethodsAttr = null;

    // JEP 181 - NestHost, NestMembers attributes since class version 55.0
    CPXAttr nestHostAttr;
    NestMembersAttr nestMembersAttr;

    // JEP 359 - Record attribute since class file 58.65535
    private RecordData recordData;

    // JEP 360 - PermittedSubclasses attribute since class file 59.65535
    private PermittedSubclassesAttr permittedSubclassesAttr;

    ModuleAttr moduleAttribute = null;
    Environment env;
    protected ConstantPool pool;

    private static final String DEFAULT_EXTENSION = ".class";
    String fileExtension = DEFAULT_EXTENSION;
    public CDOutputStream cdos;

    /**
     * Initializes the ClassData.
     *
     * @param me The constant pool reference to this class
     * @param father The constant pool reference to the super class
     * @param interfaces A list of interfaces that this class implements
     */
    public final void init(int access, ConstantPool.ConstCell me, ConstantPool.ConstCell father, ArrayList<Argument> interfaces) {
        this.access = access;

        // normalize the modifiers to access flags
        if (Modifiers.hasPseudoMod(access)) {
            createPseudoMod();
        }

        this.me = me;
        if (father == null) {
            father = pool.FindCellClassByName("java/lang/Object");
        }
        this.father = father;
        this.interfaces = interfaces;
        // Set default class file version if it is not set.
        cfv.initClassDefaults();
    }

    public final void initAsModule() {
        this.access = RuntimeConstants.ACC_MODULE;
        // this_class" module-info
        this.me = pool.FindCellClassByName("module-info");
        // super_class: zero
        this.father = new ConstantPool.ConstCell(0);
        cfv.initModuleDefaults();
    }

    /**
     * canonical default constructor
     *
     * @param env The error reporting environment.
     * @param cfv The class file version that this class file supports.
     */
    public ClassData(Environment env, CFVersion cfv) {
        super(null, 0);  // for a class, these get inited in the super - later.
        cls = this;

        this.env = env;
        this.cfv = cfv;

        pool = new ConstantPool(env);
        cdos = new CDOutputStream();
    }

    /**
     * Predicate that describes if this class has an access flag indicating that it is an
     * interface.
     *
     * @return True if the classes access flag indicates it is an interface.
     */
    public final boolean isInterface() {
        return Modifiers.isInterface(access);
    }

    /*
     * After a constant pool has been explicitly declared,
     * this method links the Constant_InvokeDynamic Constant_Dynamic
     * constants with any bootstrap methods that they index in the
     * Bootstrap Methods Attribute
     */
    protected void relinkBootstrapMethods() {
        if (bootstrapMethodsAttr == null) {
            return;
        }

        env.traceln("relinkBootstrapMethods");

        for (ConstantPool.ConstCell cell : pool) {
            ConstantPool.ConstValue ref = null;
            if (cell != null) {
                ref = cell.ref;
            }
            if (ref != null
                    && (ref.tag == ConstType.CONSTANT_INVOKEDYNAMIC || ref.tag == ConstType.CONSTANT_DYNAMIC)) {
                // Find only the Constant
                ConstantPool.ConstValue_IndyOrCondyPair refval = (ConstantPool.ConstValue_IndyOrCondyPair) ref;
                BootstrapMethodData bsmdata = refval.bsmData;
                // only care about BSM Data that were placeholders
                if (bsmdata != null && bsmdata.isPlaceholder()) {
                    // find the real BSM Data at the index
                    int bsmindex = bsmdata.placeholder_index;
                    if (bsmindex < 0 || bsmindex > bootstrapMethodsAttr.size()) {
                        // bad BSM index --
                        // give a warning, but place the index in the arg anyway
                        env.traceln("Warning: (ClassData.relinkBootstrapMethods()): Bad bootstrapMethods index: " + bsmindex);
                        // env.error("const.bsmindex", bsmindex);
                        bsmdata.arg = bsmindex;
                    } else {

                        BootstrapMethodData realbsmdata = bootstrapMethodsAttr.get(bsmindex);
                        // make the IndyPairs BSM Data point to the one from the attribute
                        refval.bsmData = realbsmdata;
                    }
                }
            }
        }
    }

    protected void numberBootstrapMethods() {
        env.traceln("Numbering Bootstrap Methods");
        if (bootstrapMethodsAttr == null) {
            return;
        }

        int index = 0;
        for (BootstrapMethodData data : bootstrapMethodsAttr) {
            data.arg = index++;
        }
    }

    // API
    // Record
    public RecordData setRecord(int where) {
        if( recordAttributeExists() ) {
            env.error(where, "warn.record.repeated");
        }
        this.recordData = new RecordData(cls);
        return this.recordData;
    }

    /**
     * Rejects a record: removes the record attribute if there are no components
     */
    public void rejectRecord() {
        this.recordData = null;
    }

    // Field
    public ConstantPool.ConstValue_Pair mkNape(ConstantPool.ConstCell name, ConstantPool.ConstCell sig) {
        return new ConstantPool.ConstValue_Pair(ConstType.CONSTANT_NAMEANDTYPE, name, sig);
    }

    public ConstantPool.ConstValue_Pair mkNape(String name, String sig) {
        return mkNape(pool.FindCellAsciz(name), pool.FindCellAsciz(sig));
    }

    public FieldData addFieldIfAbsent(int access, ConstantPool.ConstCell name, ConstantPool.ConstCell sig) {
        ConstantPool.ConstValue_Pair nape = mkNape(name, sig);
        env.traceln(" [ClassData.addFieldIfAbsent]:  #" + nape.left.arg + ":#" + nape.right.arg);
        FieldData fd = getField(nape);
        if( fd == null ) {
            env.traceln(" [ClassData.addFieldIfAbsent]:  new field.");
            fd = addField(access,nape);
        }
        return fd;
    }

    private FieldData getField(ConstantPool.ConstValue_Pair nape) {
        for (FieldData fd : fields) {
            if( fd.getNameDesc().equals(nape) ) {
                return fd;
            }
        }
        return null;
    }

    public FieldData addField(int access, ConstantPool.ConstValue_Pair nape) {
        env.traceln(" [ClassData.addField]:  #" + nape.left.arg + ":#" + nape.right.arg);
        FieldData res = new FieldData(this, access, nape);
        fields.add(res);
        return res;
    }

    public FieldData addField(int access, ConstantPool.ConstCell name, ConstantPool.ConstCell sig) {
        return addField(access, mkNape(name, sig));
    }

    public FieldData addField(int access, String name, String type) {
        return addField(access, pool.FindCellAsciz(name), pool.FindCellAsciz(type));
    }

    public ConstantPool.ConstCell LocalFieldRef(FieldData field) {
        return pool.FindCell(ConstType.CONSTANT_FIELD, me, pool.FindCell(field.getNameDesc()));
    }

    public ConstantPool.ConstCell LocalFieldRef(ConstantPool.ConstValue nape) {
        return pool.FindCell(ConstType.CONSTANT_FIELD, me, pool.FindCell(nape));
    }

    public ConstantPool.ConstCell LocalFieldRef(ConstantPool.ConstCell name, ConstantPool.ConstCell sig) {
        return LocalFieldRef(mkNape(name, sig));
    }

    public ConstantPool.ConstCell LocalFieldRef(String name, String sig) {
        return LocalFieldRef(pool.FindCellAsciz(name), pool.FindCellAsciz(sig));
    }

    MethodData curMethod;

    public MethodData StartMethod(int access, ConstantPool.ConstCell name, ConstantPool.ConstCell sig, ArrayList exc_table) {
        EndMethod();
        env.traceln(" [ClassData.StartMethod]:  #" + name.arg + ":#" + sig.arg);
        curMethod = new MethodData(this, access, name, sig, exc_table);
        methods.add(curMethod);
        return curMethod;
    }

    public void EndMethod() {
        curMethod = null;
    }

    public ConstantPool.ConstCell LocalMethodRef(ConstantPool.ConstValue nape) {
        return pool.FindCell(ConstType.CONSTANT_METHOD, me, pool.FindCell(nape));
    }

    public ConstantPool.ConstCell LocalMethodRef(ConstantPool.ConstCell name, ConstantPool.ConstCell sig) {
        return LocalMethodRef(mkNape(name, sig));
    }

    void addLocVarData(int opc, Argument arg) {
    }

    public void addInnerClass(int access, ConstantPool.ConstCell name, ConstantPool.ConstCell innerClass, ConstantPool.ConstCell outerClass) {
        env.traceln("addInnerClass (with indexes: Name (" + name.toString() + "), Inner (" + innerClass.toString() + "), Outer (" + outerClass.toString() + ").");
        if (innerClasses == null) {
            innerClasses = new DataVectorAttr<>(this, AttrTag.ATT_InnerClasses.parsekey());
        }
        innerClasses.add(new InnerClassData(access, name, innerClass, outerClass));
    }

    public void addBootstrapMethod(BootstrapMethodData bsmData) {
        env.traceln("addBootstrapMethod");
        if (bootstrapMethodsAttr == null) {
            bootstrapMethodsAttr = new DataVectorAttr<>(this, AttrTag.ATT_BootstrapMethods.parsekey());
        }
        bootstrapMethodsAttr.add(bsmData);
    }

    public void addNestHost(ConstantPool.ConstCell hostClass) {
        env.traceln("addNestHost");
        nestHostAttr = new CPXAttr(this, AttrTag.ATT_NestHost.parsekey(), hostClass);
    }

    public void addNestMembers(List<ConstantPool.ConstCell> classes) {
        env.traceln("addNestMembers");
        nestMembersAttr = new NestMembersAttr(this, classes);
    }

    public void addPermittedSubclasses(List<ConstantPool.ConstCell> classes) {
        env.traceln("addPermittedSubclasses");
        permittedSubclassesAttr = new PermittedSubclassesAttr(this, classes);
    }


    public void endClass() {
        sourceFileNameAttr = new CPXAttr(this,
                AttrTag.ATT_SourceFile.parsekey(),
                pool.FindCellAsciz(env.getSimpleInputFileName()));
        pool.NumberizePool();
        pool.CheckGlobals();
        numberBootstrapMethods();
        try {
            me = pool.uncheckedGetCell(me.arg);
            env.traceln("me=" + me);
            ConstantPool.ConstValue_Cell me_value = (ConstantPool.ConstValue_Cell) me.ref;
            ConstantPool.ConstCell ascicell = me_value.cell;
            env.traceln("ascicell=" + ascicell);
            ConstantPool.ConstValue_String me_str = (ConstantPool.ConstValue_String) ascicell.ref;
            myClassName = me_str.value;
            env.traceln("-------------------");
            env.traceln("-- Constant Pool --");
            env.traceln("-------------------");
            pool.printPool();
            env.traceln("-------------------");
            env.traceln(" ");
            env.traceln(" ");
            env.traceln("-------------------");
            env.traceln("-- Inner Classes --");
            env.traceln("-------------------");
            printInnerClasses();

        } catch (Throwable e) {
            env.traceln("check name:" + e);
            env.error("no.classname");
            e.printStackTrace();
        }
    }

    public void endModule(ModuleAttr moduleAttr) {
        moduleAttribute = moduleAttr.build();
        pool.NumberizePool();
        pool.CheckGlobals();
        myClassName = "module-info";
    }

    private void printInnerClasses() {
        if (innerClasses != null) {
            int i = 1;
            for (InnerClassData entry : innerClasses) {
                env.trace(" InnerClass[" + i + "]: (" + Modifiers.toString(entry.access, CF_Context.CTX_INNERCLASS) + "]), ");
                env.trace("Name:  " + entry.name.toString() + " ");
                env.trace("IC_info:  " + entry.innerClass.toString() + " ");
                env.trace("OC_info:  " + entry.outerClass.toString() + " ");
                env.traceln(" ");
                i += 1;
            }
        } else {
            env.traceln("<< NO INNER CLASSES >>");
        }

    }

    public void write(CheckedDataOutputStream out) throws IOException {

        // Write the header
        out.writeInt(JAVA_MAGIC);
        out.writeShort(cfv.minor_version());
        out.writeShort(cfv.major_version());

        pool.write(out);
        out.writeShort(access); // & MM_CLASS; // Q
        out.writeShort(me.arg);
        out.writeShort(father.arg);

        // Write the interface names
        if (interfaces != null) {
            out.writeShort(interfaces.size());
            for (Argument intf : interfaces) {
                out.writeShort(intf.arg);
            }
        } else {
            out.writeShort(0);
        }

        // Write the fields
        if (fields != null) {
            out.writeShort(fields.size());
            for (FieldData field : fields) {
                field.write(out);
            }
        } else {
            out.writeShort(0);
        }

        // Write the methods
        if (methods != null) {
            out.writeShort(methods.size());
            for (MethodData method : methods) {
                method.write(out);
            }
        } else {
            out.writeShort(0);
        }

        // Write the attributes
        DataVector attrs = getAttrVector();
        attrs.write(out);
    } // end ClassData.write()

    @Override
    protected DataVector getAttrVector() {
        DataVector attrs = new DataVector();
        if( moduleAttribute != null ) {
            if (annotAttrVis != null)
                attrs.add(annotAttrVis);
            if (annotAttrInv != null)
                attrs.add(annotAttrInv);
            attrs.add(moduleAttribute);
        } else {
            attrs.add(sourceFileNameAttr);
            // JEP 359 since class file 58.65535
            if( recordData != null ) {
                attrs.add(recordData);
            }
            if (innerClasses != null)
                attrs.add(innerClasses);
            if (syntheticAttr != null)
                attrs.add(syntheticAttr);
            if (deprecatedAttr != null)
                attrs.add(deprecatedAttr);
            if (annotAttrVis != null)
                attrs.add(annotAttrVis);
            if (annotAttrInv != null)
                attrs.add(annotAttrInv);
            if (type_annotAttrVis != null)
                attrs.add(type_annotAttrVis);
            if (type_annotAttrInv != null)
                attrs.add(type_annotAttrInv);
            if (bootstrapMethodsAttr != null)
                attrs.add(bootstrapMethodsAttr);
            // since class version 55.0
            if(nestHostAttributeExists())
                attrs.add(nestHostAttr);
            if(nestMembersAttributesExist())
                attrs.add(nestMembersAttr);
            // since class version 59.65535 (JEP 360)
            if ( permittedSubclassesAttributesExist() )
                attrs.add(permittedSubclassesAttr);
        }
        return attrs;
    }

    static char fileSeparator; //=System.getProperty("file.separator");

    /**
     * Writes to the directory passed with -d option
     */
    public void write(File destdir) throws IOException {
        File outfile;
        if (destdir == null) {
            int startofname = myClassName.lastIndexOf("/");
            if (startofname != -1) {
                myClassName = myClassName.substring(startofname + 1);
            }
            outfile = new File(myClassName + fileExtension);
        } else {
            env.traceln("writing -d " + destdir.getPath());
            if (fileSeparator == 0) {
                fileSeparator = System.getProperty("file.separator").charAt(0);
            }
            if (fileSeparator != '/') {
                myClassName = myClassName.replace('/', fileSeparator);
            }
            outfile = new File(destdir, myClassName + fileExtension);
            File outdir = new File(outfile.getParent());
            if (!outdir.exists() && !outdir.mkdirs()) {
                env.error("cannot.write", outdir.getPath());
                return;
            }
        }

        DataOutputStream dos = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(outfile)));
        cdos.setDataOutputStream(dos);
        try {
            write(cdos);
        } finally {
            dos.close();
        }
    }  // end write()

    public void setByteLimit(int bytelimit) {
        cdos.enable();
        cdos.setLimit(bytelimit);
    }

    public boolean nestHostAttributeExists() {
        return nestHostAttr != null;
    }

    public boolean nestMembersAttributesExist() { return nestMembersAttr != null;  }

    public boolean permittedSubclassesAttributesExist() { return permittedSubclassesAttr != null;  }

    public boolean recordAttributeExists() { return recordData != null;  }

    /**
     * This is a wrapper for DataOutputStream, used for debugging purposes. it allows
     * writing the byte-stream of a class up to a given byte number.
     */
    static private class CDOutputStream implements CheckedDataOutputStream {

        private int bytelimit;
        private DataOutputStream dos;
        public boolean enabled = false;

        public CDOutputStream() {
            dos = null;
        }

        public CDOutputStream(OutputStream out) {
            setOutputStream(out);
        }

        public final void setOutputStream(OutputStream out) {
            dos = new DataOutputStream(out);
        }

        public void setDataOutputStream(DataOutputStream dos) {
            this.dos = dos;
        }

        public void setLimit(int lim) {
            bytelimit = lim;
        }

        public void enable() {
            enabled = true;
        }

        private synchronized void check(String loc) throws IOException {
            if (enabled && dos.size() >= bytelimit) {
                throw new IOException(loc);
            }
        }

        @Override
        public synchronized void write(int b) throws IOException {
            dos.write(b);
            check("Writing byte: " + b);
        }

        @Override
        public synchronized void write(byte b[], int off, int len) throws IOException {
            dos.write(b, off, len);
            check("Writing byte-array: " + b);
        }

        @Override
        public final void writeBoolean(boolean v) throws IOException {
            dos.writeBoolean(v);
            check("Writing writeBoolean: " + (v ? "true" : "false"));
        }

        @Override
        public final void writeByte(int v) throws IOException {
            dos.writeByte(v);
            check("Writing writeByte: " + v);
        }

        @Override
        public void writeShort(int v) throws IOException {
            dos.writeShort(v);
            check("Writing writeShort: " + v);
        }

        @Override
        public void writeChar(int v) throws IOException {
            dos.writeChar(v);
            check("Writing writeChar: " + v);
        }

        @Override
        public void writeInt(int v) throws IOException {
            dos.writeInt(v);
            check("Writing writeInt: " + v);
        }

        @Override
        public void writeLong(long v) throws IOException {
            dos.writeLong(v);
            check("Writing writeLong: " + v);
        }

        @Override
        public void writeFloat(float v) throws IOException {
            dos.writeFloat(v);
            check("Writing writeFloat: " + v);
        }

        @Override
        public void writeDouble(double v) throws IOException {
            dos.writeDouble(v);
            check("Writing writeDouble: " + v);
        }

        @Override
        public void writeBytes(String s) throws IOException {
            dos.writeBytes(s);
            check("Writing writeBytes: " + s);
        }

        @Override
        public void writeChars(String s) throws IOException {
            dos.writeChars(s);
            check("Writing writeChars: " + s);
        }

        @Override
        public void writeUTF(String s) throws IOException {
            dos.writeUTF(s);
            check("Writing writeUTF: " + s);
        }
    }
}// end class ClassData

