/*
 * Bytecode Analysis Framework
 * Copyright (C) 2003,2004 University of Maryland
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba;

import java.util.IdentityHashMap;

import org.apache.bcel.classfile.LineNumber;
import org.apache.bcel.classfile.LineNumberTable;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.MethodGen;

import edu.umd.cs.findbugs.SystemProperties;

/**
 * Summarize line numbers (and other source information) for a method.
 */
public class LineNumberMap {
    /**
     * Set this property to true to get debug print statements.
     */
    private static final boolean DEBUG = SystemProperties.getBoolean("lnm.debug");

    /**
     * When this is true, the workaround for the bug in BCEL 5.0's
     * LineNumberTable class is disabled.
     */
    private static final boolean LINE_NUMBER_BUG = SystemProperties.getBoolean("lineNumberBug");

    private MethodGen methodGen;

    private IdentityHashMap<InstructionHandle, LineNumber> lineNumberMap;

    private boolean hasLineNumbers;

    /**
     * Constructor.
     * 
     * @param methodGen
     *            the method to summarize line numbers for
     */
    public LineNumberMap(MethodGen methodGen) {
        this.methodGen = methodGen;
        lineNumberMap = new IdentityHashMap<InstructionHandle, LineNumber>();
        hasLineNumbers = false;
    }

    /**
     * Build the line number information. Should be called before any other
     * methods.
     */
    public void build() {
        int numGood = 0, numBytecodes = 0;

        if (DEBUG) {
            System.out.println("Method: " + methodGen.getName() + " - " + methodGen.getSignature() + "in class "
                    + methodGen.getClassName());
        }

        // Associate line number information with each InstructionHandle
        LineNumberTable table = methodGen.getLineNumberTable(methodGen.getConstantPool());

        if (table != null && table.getTableLength() > 0) {
            checkTable(table);
            InstructionHandle handle = methodGen.getInstructionList().getStart();
            while (handle != null) {
                int bytecodeOffset = handle.getPosition();
                if (bytecodeOffset < 0)
                    throw new IllegalStateException("Bad bytecode offset: " + bytecodeOffset);
                if (DEBUG)
                    System.out.println("Looking for source line for bytecode offset " + bytecodeOffset);
                int sourceLine;
                try {
                    sourceLine = table.getSourceLine(bytecodeOffset);
                } catch (ArrayIndexOutOfBoundsException e) {
                    if (LINE_NUMBER_BUG)
                        throw e;
                    else
                        sourceLine = -1;
                }
                if (sourceLine >= 0)
                    ++numGood;
                lineNumberMap.put(handle, new LineNumber(bytecodeOffset, sourceLine));
                handle = handle.getNext();
                ++numBytecodes;
            }
            hasLineNumbers = true;

            if (DEBUG)
                System.out.println("\t" + numGood + "/" + numBytecodes + " had valid line numbers");
        }
    }

    private void checkTable(LineNumberTable table) {
        if (DEBUG)
            System.out.println("line number table has length " + table.getTableLength());
        LineNumber[] entries = table.getLineNumberTable();
        int lastBytecode = -1;
        for (int i = 0; i < entries.length; ++i) {
            LineNumber ln = entries[i];
            if (DEBUG)
                System.out.println("Entry " + i + ": pc=" + ln.getStartPC() + ", line=" + ln.getLineNumber());
            int pc = ln.getStartPC();
            if (pc <= lastBytecode)
                throw new IllegalStateException("LineNumberTable is not sorted");
        }
    }

    /**
     * Does this method have line number information?
     */
    public boolean hasLineNumbers() {
        return hasLineNumbers;
    }

    /**
     * Find the line number information for instruction whose handle is given.
     * 
     * @param handle
     *            the InstructionHandle
     * @return the LineNumber object containing bytecode offset and source line
     *         number
     */
    public LineNumber lookupLineNumber(InstructionHandle handle) {
        return lineNumberMap.get(handle);
    }
}
