/*
 * 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 org.openjdk.asmtools.jasm.OpcodeTables.Opcode;
import org.openjdk.asmtools.jasm.Tables.AttrTag;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;

import static org.openjdk.asmtools.jasm.RuntimeConstants.SPLIT_VERIFIER_CFV;

class CodeAttr extends AttrData {

    protected ClassData cls;

    protected MethodData mtd;
    protected Environment env;
    protected Argument max_stack, max_locals;
    protected Instr zeroInstr, lastInstr;
    protected int cur_pc = 0;
    protected DataVector<TrapData> trap_table; // TrapData
    protected DataVectorAttr<LineNumData> lin_num_tb; // LineNumData
    protected int lastln = 0;
    protected DataVectorAttr<LocVarData> loc_var_tb;  // LocVarData
    protected DataVector<DataVectorAttr<? extends Data>> attrs;
    protected ArrayList<Integer> slots;
    protected HashMap<String, LocVarData> locvarsHash;
    protected HashMap<String, Label> labelsHash;
    protected HashMap<String, Trap> trapsHash;
    protected StackMapData curMapEntry = null;
    protected DataVectorAttr<StackMapData> stackMap;
    // type annotations
    protected DataVectorAttr<TypeAnnotationData> type_annotAttrVis = null;
    protected DataVectorAttr<TypeAnnotationData> type_annotAttrInv = null;

    public CodeAttr(MethodData mtd, int pos, int paramcnt, Argument max_stack, Argument max_locals) {
        super(mtd.cls, AttrTag.ATT_Code.parsekey());
        this.mtd = mtd;
        this.cls = mtd.cls;
        this.env = cls.env;
        this.max_stack = max_stack;
        this.max_locals = max_locals;
        lastInstr = zeroInstr = new Instr();
        trap_table = new DataVector<>(0); // TrapData
        attrs = new DataVector<>();
        if (env.debugInfoFlag) {
            lin_num_tb = new DataVectorAttr<>(cls, AttrTag.ATT_LineNumberTable.parsekey());
            attrs.add(lin_num_tb);
        }
        slots = new ArrayList<>(paramcnt);
        for (int k = 0; k < paramcnt; k++) {
            slots.add(k, 1);
        }
    }

    void endCode() {
        checkTraps();
        checkLocVars();
        checkLabels();
        //
        if (type_annotAttrVis != null) {
            attrs.add(type_annotAttrVis);
        }
        if (type_annotAttrInv != null) {
            attrs.add(type_annotAttrInv);
        }
    }

    public void addAnnotations(ArrayList<AnnotationData> list) {
        for (AnnotationData item : list) {
            boolean invisible = item.invisible;
            if (item instanceof TypeAnnotationData) {
                // Type Annotations
                TypeAnnotationData ta = (TypeAnnotationData) item;
                if (invisible) {
                    if (type_annotAttrInv == null) {
                        type_annotAttrInv = new DataVectorAttr(cls,
                                AttrTag.ATT_RuntimeInvisibleTypeAnnotations.parsekey());
                    }
                    type_annotAttrInv.add(ta);
                } else {
                    if (type_annotAttrVis == null) {
                        type_annotAttrVis = new DataVectorAttr(cls,
                                AttrTag.ATT_RuntimeVisibleTypeAnnotations.parsekey());
                    }
                    type_annotAttrVis.add(ta);
                }
            }
        }
    }

    /* -------------------------------------- Traps */
    Trap trapDecl(int pos, String name) {
        Trap local;
        if (trapsHash == null) {
            trapsHash = new HashMap<>(10);
            local = null;
        } else {
            local = trapsHash.get(name);
        }
        if (local == null) {
            local = new Trap(pos, name);
            trapsHash.put(name, local);
        }
        return local;
    }

    void beginTrap(int pos, String name) {
        Trap trap = trapDecl(pos, name);
        if (trap.start_pc != Argument.NotSet) {
            env.error("trap.tryredecl", name);
            return;
        }
        trap.start_pc = cur_pc;
    }

    void endTrap(int pos, String name) {
        Trap trap = trapDecl(pos, name);
        if (trap.end_pc != Argument.NotSet) {
            env.error("trap.endtryredecl", name);
            return;
        }
        trap.end_pc = cur_pc;
    }

    void trapHandler(int pos, String name, Argument type) {
        Trap trap = trapDecl(pos, name);
        trap.refd = true;
        TrapData trapData = new TrapData(pos, trap, cur_pc, type);
        trap_table.addElement(trapData);
    }

    void checkTraps() {
        if (trapsHash == null) {
            return;
        }
        for (Trap trap : trapsHash.values()) {
            if (!trap.refd) {
                env.error(trap.pos, "warn.trap.notref", trap.name);
            }
        }

        for (TrapData trapData : trap_table) {
            Trap trapLabel = trapData.trap;
            if (trapLabel.start_pc == Argument.NotSet) {
                env.error(trapData.pos, "trap.notry", trapLabel.name);
            }
            if (trapLabel.end_pc == Argument.NotSet) {
                env.error(trapData.pos, "trap.noendtry", trapLabel.name);
            }
        }
    }

    /* -------------------------------------- Labels */
    Label labelDecl(String name) {
        Label local;
        if (labelsHash == null) {
            labelsHash = new HashMap<>(10);
            local = null;
        } else {
            local = labelsHash.get(name);
        }
        if (local == null) {
            local = new Label(name);
            labelsHash.put(name, local);
        }
        return local;
    }

    public Label LabelDef(int pos, String name) {
        Label label = labelDecl(name);
        if (label.defd) {
            env.error(pos, "label.redecl", name);
            return null;
        }
        label.defd = true;
        label.arg = cur_pc;
        return label;
    }

    public Label LabelRef(String name) {
        Label label = labelDecl(name);
        label.refd = true;
        return label;
    }

    void checkLabels() {
        if (labelsHash == null) {
            return;
        }

        for (Label local : labelsHash.values()) {
            // check that every label is defined
            if (!local.defd) {
                env.error("label.undecl", local.name);
            }
        }
    }

    /* -------------------------------------- Variables */
    LocVarData locvarDecl(String name) {
        LocVarData local;
        if (locvarsHash == null) {
            locvarsHash = new HashMap<>(10);
            local = null;
        } else {
            local = locvarsHash.get(name);
        }
        if (local == null) {
            local = new LocVarData(name);
            locvarsHash.put(name, local);
        }
        return local;
    }

    public void LocVarDataDef(int slot) {
        slots.set(slot, 1);
        if ((max_locals != null) && (max_locals.arg < slots.size())) {
            env.error("warn.illslot", Integer.toString(slot));
        }
    }

    public void LocVarDataDef(String name, ConstantPool.ConstCell type) {
        LocVarData locvar = locvarDecl(name);
        if (locvar.defd) {
            env.error("locvar.redecl", name);
            return;
        }
        locvar.defd = true;
        locvar.start_pc = (short) cur_pc;
        locvar.name_cpx = cls.pool.FindCellAsciz(name);
        locvar.sig_cpx = type;
        int k;
        findSlot:
        {
            for (k = 0; k < slots.size(); k++) {
                if (slots.get(k) == 0) {
                    break findSlot;
                }
            }
            k = slots.size();
        }
        LocVarDataDef(k);
        locvar.arg = k;
        if (loc_var_tb == null) {
            loc_var_tb = new DataVectorAttr<>(cls, AttrTag.ATT_LocalVariableTable.parsekey());
            attrs.add(loc_var_tb);
        }
        loc_var_tb.add(locvar);
    }

    public Argument LocVarDataRef(String name) {
        LocVarData locvar = locvarDecl(name);
        if (!locvar.defd) {
            env.error("locvar.undecl", name);
            locvar.defd = true; // to avoid multiple error messages
        }
        locvar.refd = true;
        return locvar;
    }

    public void LocVarDataEnd(int slot) {
        slots.set(slot, 0);
    }

    public void LocVarDataEnd(String name) {
        LocVarData locvar = locvarsHash.get(name);
        if (locvar == null) {
            env.error("locvar.undecl", name);
            return;
        } else if (!locvar.defd) {
            env.error("locvar.undecl", name);
            return;
        }
        locvar.length = (short) (cur_pc - locvar.start_pc);

        slots.set(locvar.arg, 0);
        locvarsHash.put(name, new LocVarData(name));
    }

    void checkLocVars() {
        if (locvarsHash == null) {
            return;
        }
        for (LocVarData locvar : locvarsHash.values()) {
            if (!locvar.defd) {
                continue;
            } // this is false locvar
            // set end of scope, if not set
            if (slots.get(locvar.arg) == 1) {
                locvar.length = (short) (cur_pc - locvar.start_pc);
                slots.set(locvar.arg, 0);
            }
        }
    }

    /* -------------------------------------- StackMap */
    public StackMapData getStackMap() {
        if (curMapEntry == null) {
            curMapEntry = new StackMapData(env);
            if (cls.cfv.major_version() >= SPLIT_VERIFIER_CFV) {
                curMapEntry.setIsStackMapTable(true);
            }
        }
        return curMapEntry;
    }

    /*====================================================== Instr */
    void addInstr(int mnenoc_pos, Opcode opcode, Argument arg, Object arg2) {
        Instr newInstr = new Instr(cur_pc, cls.env.pos, opcode, arg, arg2);
        lastInstr.next = newInstr;
        lastInstr = newInstr;
        int len = opcode.length();
        switch (opcode) {
            case opc_tableswitch:
                len = ((SwitchTable) arg2).recalcTableSwitch(cur_pc);
                break;
            case opc_lookupswitch:
                len = ((SwitchTable) arg2).calcLookupSwitch(cur_pc);
                break;
            case opc_ldc:
                ((ConstantPool.ConstCell) arg).setRank(0);
                break;
            default:
                if (arg instanceof ConstantPool.ConstCell) {
                    ((ConstantPool.ConstCell) arg).setRank(1);
                }
        }
        if (env.debugInfoFlag) {
            int ln = env.lineNumber(mnenoc_pos);
            if (ln != lastln) { // only one entry in lin_num_tb per line
                lin_num_tb.add(new LineNumData(cur_pc, ln));
                lastln = ln;
            }
        }
        if (curMapEntry != null) {
            curMapEntry.pc = cur_pc;
            StackMapData prevStackFrame = null;
            if (stackMap == null) {
                if (cls.cfv.major_version() >= SPLIT_VERIFIER_CFV) {
                    stackMap = new DataVectorAttr<>(cls, AttrTag.ATT_StackMapTable.parsekey());
                } else {
                    stackMap = new DataVectorAttr<>(cls, AttrTag.ATT_StackMap.parsekey());
                }
                attrs.add(stackMap);
            } else if (stackMap.size() > 0) {
                prevStackFrame = stackMap.get(stackMap.size() - 1);
            }
            curMapEntry.setOffset(prevStackFrame);
            stackMap.add(curMapEntry);
            curMapEntry = null;
        }

        cur_pc += len;
    }

    /*====================================================== Attr interface */
    // subclasses must redefine this
    @Override
    public int attrLength() {
        return 2 + 2 + 4 // for max_stack, max_locals, and cur_pc
                + cur_pc //      + 2+trap_table.size()*8
                + trap_table.getLength() + attrs.getLength();
    }

    @Override
    public void write(CheckedDataOutputStream out)
            throws IOException, Parser.CompilerError {
        int mxstck = (max_stack != null) ? max_stack.arg : 0;
        int mxloc = (max_locals != null) ? max_locals.arg : slots.size();
        super.write(out);  // attr name, attr len
        out.writeShort(mxstck);
        out.writeShort(mxloc);
        out.writeInt(cur_pc);
        for (Instr instr = zeroInstr.next; instr != null; instr = instr.next) {
            instr.write(out, env);
        }

        trap_table.write(out);

        attrs.write(out);
    }

    /*-------------------------------------------------------- */
    /* CodeAttr inner classes */
    static public class Local extends Argument {

        String name;
        boolean defd = false, refd = false;

        public Local(String name) {
            this.name = name;
        }
    }

    /**
     *
     */
    static public class Label extends Local {

        public Label(String name) {
            super(name);
        }
    }

    /**
     *
     */
    class LocVarData extends Local implements Data {

        // arg means slot
        short start_pc, length;
        ConstantPool.ConstCell name_cpx, sig_cpx;

        public LocVarData(String name) {
            super(name);
        }

        @Override
        public int getLength() {
            return 10;
        }

        @Override
        public void write(CheckedDataOutputStream out) throws IOException {
            out.writeShort(start_pc);
            out.writeShort(length);
            out.writeShort(name_cpx.arg);
            out.writeShort(sig_cpx.arg);
            out.writeShort(arg);
        }
    }

    /**
     *
     */
    class LineNumData implements Data {

        int start_pc, line_number;

        public LineNumData(int start_pc, int line_number) {
            this.start_pc = start_pc;
            this.line_number = line_number;
        }

        @Override
        public int getLength() {
            return 4;
        }

        @Override
        public void write(CheckedDataOutputStream out) throws IOException {
            out.writeShort(start_pc);
            out.writeShort(line_number);
        }
    }

    /**
     *
     */
    class Trap extends Local {

        int start_pc = Argument.NotSet, end_pc = Argument.NotSet;
        int pos;

        Trap(int pos, String name) {
            super(name);
            this.pos = pos;
        }
    }

    /**
     *
     */
    class TrapData implements Data {

        int pos;
        Trap trap;
        int handler_pc;
        Argument catchType;

        public TrapData(int pos, Trap trap, int handler_pc, Argument catchType) {
            this.pos = pos;
            this.trap = trap;
            this.handler_pc = handler_pc;
            this.catchType = catchType;
        }

        @Override
        public int getLength() {
            return 8; // add the length of number of elements
        }

        @Override
        public void write(CheckedDataOutputStream out) throws IOException {
            out.writeShort(trap.start_pc);
            out.writeShort(trap.end_pc);
            out.writeShort(handler_pc);
            if (catchType.isSet()) {
                out.writeShort(catchType.arg);
            } else {
                out.writeShort(0);
            }
        }
    }  // end TrapData
} // end CodeAttr

