/*
 *  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
 *
 *     http://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.
 */
package org.apache.harmony.pack200;

import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.objectweb.asm.Label;

/**
 * Bytecode bands (corresponds to the <code>bc_bands</code> set of bands in the
 * pack200 specification)
 */
public class BcBands extends BandSet {

    private final CpBands cpBands;
    private final Segment segment;

    public BcBands(CpBands cpBands, Segment segment, int effort) {
        super(effort, segment.getSegmentHeader());
        this.cpBands = cpBands;
        this.segment = segment;
    }

    private final IntList bcCodes = new IntList();
    private final IntList bcCaseCount = new IntList();
    private final IntList bcCaseValue = new IntList();
    private final IntList bcByte = new IntList();
    private final IntList bcShort = new IntList();
    private final IntList bcLocal = new IntList();
    private final List bcLabel = new ArrayList();
    private final List bcIntref = new ArrayList();
    private final List bcFloatRef = new ArrayList();
    private final List bcLongRef = new ArrayList();
    private final List bcDoubleRef = new ArrayList();
    private final List bcStringRef = new ArrayList();
    private final List bcClassRef = new ArrayList();
    private final List bcFieldRef = new ArrayList();
    private final List bcMethodRef = new ArrayList();
    private final List bcIMethodRef = new ArrayList();
    private List bcThisField = new ArrayList();
    private final List bcSuperField = new ArrayList();
    private List bcThisMethod = new ArrayList();
    private List bcSuperMethod = new ArrayList();
    private List bcInitRef = new ArrayList();

    private String currentClass;
    private String superClass;
    private String currentNewClass;

    private static final int MULTIANEWARRAY = 197;
    private static final int ALOAD_0 = 42;
    private static final int WIDE = 196;
    private static final int INVOKEINTERFACE = 185;
    private static final int TABLESWITCH = 170;
    private static final int IINC = 132;
    private static final int LOOKUPSWITCH = 171;
    private static final int endMarker = 255;

    private final IntList bciRenumbering = new IntList();
    private final Map labelsToOffsets = new HashMap();
    private int byteCodeOffset;
    private int renumberedOffset;
    private final IntList bcLabelRelativeOffsets = new IntList();

    public void setCurrentClass(String name, String superName) {
        currentClass = name;
        superClass = superName;
    }

    /**
     * All input classes for the segment have now been read in, so this method
     * is called so that this class can calculate/complete anything it could not
     * do while classes were being read.
     */
    public void finaliseBands() {
        bcThisField = getIndexInClass(bcThisField);
        bcThisMethod = getIndexInClass(bcThisMethod);
        bcSuperMethod = getIndexInClass(bcSuperMethod);
        bcInitRef = getIndexInClassForConstructor(bcInitRef);
    }

    public void pack(OutputStream out) throws IOException, Pack200Exception {
        PackingUtils.log("Writing byte code bands...");
        byte[] encodedBand = encodeBandInt("bcCodes", bcCodes.toArray(),
                Codec.BYTE1);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length + " bytes from bcCodes["
                + bcCodes.size() + "]");

        encodedBand = encodeBandInt("bcCaseCount", bcCaseCount.toArray(),
                Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcCaseCount[" + bcCaseCount.size() + "]");

        encodedBand = encodeBandInt("bcCaseValue", bcCaseValue.toArray(),
                Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcCaseValue[" + bcCaseValue.size() + "]");

        encodedBand = encodeBandInt("bcByte", bcByte.toArray(), Codec.BYTE1);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length + " bytes from bcByte["
                + bcByte.size() + "]");

        encodedBand = encodeBandInt("bcShort", bcShort.toArray(), Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length + " bytes from bcShort["
                + bcShort.size() + "]");

        encodedBand = encodeBandInt("bcLocal", bcLocal.toArray(),
                Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length + " bytes from bcLocal["
                + bcLocal.size() + "]");

        encodedBand = encodeBandInt("bcLabel", integerListToArray(bcLabel),
                Codec.BRANCH5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length + " bytes from bcLabel["
                + bcLabel.size() + "]");

        encodedBand = encodeBandInt("bcIntref", cpEntryListToArray(bcIntref),
                Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcIntref[" + bcIntref.size() + "]");

        encodedBand = encodeBandInt("bcFloatRef",
                cpEntryListToArray(bcFloatRef), Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcFloatRef[" + bcFloatRef.size() + "]");

        encodedBand = encodeBandInt("bcLongRef", cpEntryListToArray(bcLongRef),
                Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcLongRef[" + bcLongRef.size() + "]");

        encodedBand = encodeBandInt("bcDoubleRef",
                cpEntryListToArray(bcDoubleRef), Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcDoubleRef[" + bcDoubleRef.size() + "]");

        encodedBand = encodeBandInt("bcStringRef",
                cpEntryListToArray(bcStringRef), Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcStringRef[" + bcStringRef.size() + "]");

        encodedBand = encodeBandInt("bcClassRef",
                cpEntryOrNullListToArray(bcClassRef), Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcClassRef[" + bcClassRef.size() + "]");

        encodedBand = encodeBandInt("bcFieldRef",
                cpEntryListToArray(bcFieldRef), Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcFieldRef[" + bcFieldRef.size() + "]");

        encodedBand = encodeBandInt("bcMethodRef",
                cpEntryListToArray(bcMethodRef), Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcMethodRef[" + bcMethodRef.size() + "]");

        encodedBand = encodeBandInt("bcIMethodRef",
                cpEntryListToArray(bcIMethodRef), Codec.DELTA5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcIMethodRef[" + bcIMethodRef.size() + "]");

        encodedBand = encodeBandInt("bcThisField",
                integerListToArray(bcThisField), Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcThisField[" + bcThisField.size() + "]");

        encodedBand = encodeBandInt("bcSuperField",
                integerListToArray(bcSuperField), Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcSuperField[" + bcSuperField.size() + "]");

        encodedBand = encodeBandInt("bcThisMethod",
                integerListToArray(bcThisMethod), Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcThisMethod[" + bcThisMethod.size() + "]");

        encodedBand = encodeBandInt("bcSuperMethod",
                integerListToArray(bcSuperMethod), Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcSuperMethod[" + bcSuperMethod.size() + "]");

        encodedBand = encodeBandInt("bcInitRef", integerListToArray(bcInitRef),
                Codec.UNSIGNED5);
        out.write(encodedBand);
        PackingUtils.log("Wrote " + encodedBand.length
                + " bytes from bcInitRef[" + bcInitRef.size() + "]");

        // out.write(encodeBandInt(cpEntryintegerListToArray(bcEscRef),
        // Codec.UNSIGNED5));
        // out.write(encodeBandInt(integerListToArray(bcEscRefSize),
        // Codec.UNSIGNED5));
        // out.write(encodeBandInt(integerListToArray(bcEscSize),
        // Codec.UNSIGNED5));
        // out.write(encodeBandInt(integerListToArray(bcEscByte), Codec.BYTE1));
    }

    private List getIndexInClass(List cPMethodOrFieldList) {
        List indices = new ArrayList(cPMethodOrFieldList.size());
        for (int i = 0; i < cPMethodOrFieldList.size(); i++) {
            CPMethodOrField cpMF = (CPMethodOrField) cPMethodOrFieldList.get(i);
            indices.add(new Integer(cpMF.getIndexInClass()));
        }
        return indices;
    }

    private List getIndexInClassForConstructor(List cPMethodList) {
        List indices = new ArrayList(cPMethodList.size());
        for (int i = 0; i < cPMethodList.size(); i++) {
            CPMethodOrField cpMF = (CPMethodOrField) cPMethodList.get(i);
            indices.add(new Integer(cpMF.getIndexInClassForConstructor()));
        }
        return indices;
    }

    public void visitEnd() {
        for (int i = 0; i < bciRenumbering.size(); i++) {
            if (bciRenumbering.get(i) == -1) {
                bciRenumbering.remove(i);
                bciRenumbering.add(i, ++renumberedOffset);
            }
        }
        if (renumberedOffset != 0) {
            if(renumberedOffset + 1 != bciRenumbering.size()) {
                throw new RuntimeException("Mistake made with renumbering");
            }
            for (int i = bcLabel.size() - 1; i >= 0; i--) {
                Object label = bcLabel.get(i);
                if (label instanceof Integer) {
                    break;
                } else if (label instanceof Label) {
                    bcLabel.remove(i);
                    Integer offset = (Integer) labelsToOffsets.get(label);
                    int relativeOffset = bcLabelRelativeOffsets.get(i);
                    bcLabel.add(i, new Integer(bciRenumbering.get(offset.intValue()) - bciRenumbering.get(relativeOffset)));
                }
            }
            bcCodes.add(endMarker);
            segment.getClassBands().doBciRenumbering(bciRenumbering,
                    labelsToOffsets);
            bciRenumbering.clear();
            labelsToOffsets.clear();
            byteCodeOffset = 0;
            renumberedOffset = 0;
        }
    }

    public void visitLabel(Label label) {
        labelsToOffsets.put(label, new Integer(byteCodeOffset));
    }

    public void visitFieldInsn(int opcode, String owner, String name,
            String desc) {
        byteCodeOffset += 3;
        updateRenumbering();
        boolean aload_0 = false;
        if (bcCodes.size() > 0
                && (bcCodes.get(bcCodes.size() - 1)) == ALOAD_0) {
            bcCodes.remove(bcCodes.size() - 1);
            aload_0 = true;
        }
        CPMethodOrField cpField = cpBands.getCPField(owner, name, desc);
        if (aload_0) {
            opcode += 7;
        }
        if (owner.equals(currentClass)) {
            opcode += 24; // change to getstatic_this, putstatic_this etc.
            bcThisField.add(cpField);
//        } else if (owner.equals(superClass)) {
//            opcode += 38; // change to getstatic_super etc.
//            bcSuperField.add(cpField);
        } else {
            if (aload_0) {
                opcode -= 7;
                bcCodes.add(ALOAD_0); // add aload_0 back in because
                // there's no special rewrite in
                // this case.
            }
            bcFieldRef.add(cpField);
        }
        aload_0 = false;
        bcCodes.add(opcode);
    }

    private void updateRenumbering() {
        if(bciRenumbering.isEmpty()) {
            bciRenumbering.add(0);
        }
        renumberedOffset ++;
        for (int i = bciRenumbering.size(); i < byteCodeOffset; i++) {
            bciRenumbering.add(-1);
        }
        bciRenumbering.add(renumberedOffset);
    }

    public void visitIincInsn(int var, int increment) {
        if (var > 255 || increment > 255) {
            byteCodeOffset += 6;
            bcCodes.add(WIDE);
            bcCodes.add(IINC);
            bcLocal.add(var);
            bcShort.add(increment);
        } else {
            byteCodeOffset += 3;
            bcCodes.add(IINC);
            bcLocal.add(var);
            bcByte.add(increment & 0xFF);
        }
        updateRenumbering();
    }

    public void visitInsn(int opcode) {
        if (opcode >= 202) {
            throw new RuntimeException(
                    "Non-standard bytecode instructions not supported");
        } else {
            bcCodes.add(opcode);
            byteCodeOffset++;
            updateRenumbering();
        }
    }

    public void visitIntInsn(int opcode, int operand) {
        switch (opcode) {
        case 17: // sipush
            bcCodes.add(opcode);
            bcShort.add(operand);
            byteCodeOffset += 3;
            break;
        case 16: // bipush
        case 188: // newarray
            bcCodes.add(opcode);
            bcByte.add(operand & 0xFF);
            byteCodeOffset += 2;
        }
        updateRenumbering();
    }

    public void visitJumpInsn(int opcode, Label label) {
        bcCodes.add(opcode);
        bcLabel.add(label);
        bcLabelRelativeOffsets.add(byteCodeOffset);
        byteCodeOffset += 3;
        updateRenumbering();
    }

    public void visitLdcInsn(Object cst) {
        CPConstant constant = cpBands.getConstant(cst);
        if (segment.lastConstantHadWideIndex() || constant instanceof CPLong
                || constant instanceof CPDouble) {
            byteCodeOffset += 3;
            if (constant instanceof CPInt) {
                bcCodes.add(237); // ildc_w
                bcIntref.add(constant);
            } else if (constant instanceof CPFloat) {
                bcCodes.add(238); // fldc
                bcFloatRef.add(constant);
            } else if (constant instanceof CPLong) {
                bcCodes.add(20); // lldc2_w
                bcLongRef.add(constant);
            } else if (constant instanceof CPDouble) {
                bcCodes.add(239); // dldc2_w
                bcDoubleRef.add(constant);
            } else if (constant instanceof CPString) {
                bcCodes.add(19); // aldc
                bcStringRef.add(constant);
            } else if (constant instanceof CPClass) {
                bcCodes.add(236); // cldc
                bcClassRef.add(constant);
            } else {
                throw new RuntimeException("Constant should not be null");
            }
        } else {
            byteCodeOffset += 2;
            if (constant instanceof CPInt) {
                bcCodes.add(234); // ildc
                bcIntref.add(constant);
            } else if (constant instanceof CPFloat) {
                bcCodes.add(235); // fldc
                bcFloatRef.add(constant);
            } else if (constant instanceof CPString) {
                bcCodes.add(18); // aldc
                bcStringRef.add(constant);
            } else if (constant instanceof CPClass) {
                bcCodes.add(233); // cldc
                bcClassRef.add(constant);
            }
        }
        updateRenumbering();
    }

    public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
        bcCodes.add(LOOKUPSWITCH);
        bcLabel.add(dflt);
        bcLabelRelativeOffsets.add(byteCodeOffset);
        bcCaseCount.add(keys.length);
        for (int i = 0; i < labels.length; i++) {
            bcCaseValue.add(keys[i]);
            bcLabel.add(labels[i]);
            bcLabelRelativeOffsets.add(byteCodeOffset);
        }
        int padding = (byteCodeOffset + 1) % 4 == 0 ? 0 : 4 - ((byteCodeOffset + 1) % 4);
        byteCodeOffset += 1 + padding + 8 + 8 * keys.length;
        updateRenumbering();
    }

    public void visitMethodInsn(int opcode, String owner, String name,
            String desc) {
        byteCodeOffset += 3;
        switch (opcode) {
        case 182: // invokevirtual
        case 183: // invokespecial
        case 184: // invokestatic
            boolean aload_0 = false;
            if (bcCodes.size() > 0
                    && (bcCodes.get(bcCodes.size() - 1))
                             == (ALOAD_0)) {
                bcCodes.remove(bcCodes.size() - 1);
                aload_0 = true;
                opcode += 7;
            }
            if (owner.equals(currentClass)) {
                opcode += 24; // change to invokevirtual_this,
                // invokespecial_this etc.

                if(name.equals("<init>") && opcode == 207) {
                    opcode = 230; // invokespecial_this_init
                    bcInitRef.add(cpBands.getCPMethod(owner, name, desc));
                } else {
                    bcThisMethod.add(cpBands.getCPMethod(owner, name, desc));
                }
            } else if (owner.equals(superClass)) { // TODO
                opcode += 38; // change to invokevirtual_super,
                // invokespecial_super etc.
                if(name.equals("<init>") && opcode == 221) {
                    opcode = 231; // invokespecial_super_init
                    bcInitRef.add(cpBands.getCPMethod(owner, name, desc));
                } else {
                    bcSuperMethod.add(cpBands.getCPMethod(owner, name, desc));
                }
            } else {
                if (aload_0) {
                    opcode -= 7;
                    bcCodes.add(ALOAD_0); // add aload_0 back in
                    // because there's no
                    // special rewrite in this
                    // case.
                }
                if(name.equals("<init>") && opcode == 183 && owner.equals(currentNewClass)) {
                    opcode = 232; // invokespecial_new_init
                    bcInitRef.add(cpBands.getCPMethod(owner, name, desc));
                } else {
                    bcMethodRef.add(cpBands.getCPMethod(owner, name, desc));
                }
            }
            bcCodes.add(opcode);
            break;
        case 185: // invokeinterface
            byteCodeOffset += 2;
            CPMethodOrField cpIMethod = cpBands.getCPIMethod(owner, name, desc);
            bcIMethodRef.add(cpIMethod);
            bcCodes.add(INVOKEINTERFACE);
            break;
        }
        updateRenumbering();
    }

    public void visitMultiANewArrayInsn(String desc, int dimensions) {
        byteCodeOffset += 4;
        updateRenumbering();
        bcCodes.add(MULTIANEWARRAY);
        bcClassRef.add(cpBands.getCPClass(desc));
        bcByte.add(dimensions & 0xFF);
    }

    public void visitTableSwitchInsn(int min, int max, Label dflt,
            Label[] labels) {
        bcCodes.add(TABLESWITCH);
        bcLabel.add(dflt);
        bcLabelRelativeOffsets.add(byteCodeOffset);
        bcCaseValue.add(min);
        int count = labels.length;
        bcCaseCount.add(count);
        for (int i = 0; i < count; i++) {
            bcLabel.add(labels[i]);
            bcLabelRelativeOffsets.add(byteCodeOffset);
        }
        int padding = byteCodeOffset % 4 == 0 ? 0 : 4 - (byteCodeOffset % 4);
        byteCodeOffset+= (padding + 12 + 4 * labels.length);
        updateRenumbering();
    }

    public void visitTypeInsn(int opcode, String type) {
        // NEW, ANEWARRAY, CHECKCAST or INSTANCEOF
        byteCodeOffset += 3;
        updateRenumbering();
        bcCodes.add(opcode);
        bcClassRef.add(cpBands.getCPClass(type));
        if(opcode == 187) { // NEW
            currentNewClass = type;
        }
    }

    public void visitVarInsn(int opcode, int var) {
        // ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, ISTORE, LSTORE, FSTORE, DSTORE, ASTORE or RET
        if (var > 255) {
            byteCodeOffset += 4;
            bcCodes.add(WIDE);
            bcCodes.add(opcode);
            bcLocal.add(var);
        } else {
            if(var > 3 || opcode == 169 /* RET */) {
                byteCodeOffset += 2;
                bcCodes.add(opcode);
                bcLocal.add(var);
            } else {
                byteCodeOffset +=1;
                switch(opcode) {
                case 21: // ILOAD
                case 54: // ISTORE
                    bcCodes.add(opcode + 5 + var);
                    break;
                case 22: // LLOAD
                case 55: // LSTORE
                    bcCodes.add(opcode + 8 + var);
                    break;
                case 23: // FLOAD
                case 56: // FSTORE
                    bcCodes.add(opcode + 11 + var);
                    break;
                case 24: // DLOAD
                case 57: // DSTORE
                    bcCodes.add(opcode + 14 + var);
                    break;
                case 25: // A_LOAD
                case 58: // A_STORE
                    bcCodes.add(opcode + 17 + var);
                    break;
                }
            }
        }
        updateRenumbering();
    }

}
