/*
 *   
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program 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 program 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 at /legal/license.txt). 
 * 
 * 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 Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */


/*
 *   
 */

import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.table.*;
import javax.swing.event.*;
import javax.swing.text.NumberFormatter;

class Profile {
    /**
     * This hashtable is a collection of all CallGraphNodes in the callgraph.
     * It's indexed by each CallGraphNode's <b>index</b> member, whose type
     * is Integer.
     */
    Hashtable nodes;

    /**
     * This hashtable is a collection of all CallGraphNodes in the callgraph.
     * It's indexed by each CallGraphNodes's <b>name</b>, whose type is
     * String. <p>
     *
     * Each entry in this hashtable is either a CallGraphNodes (if only
     * one CallGraphNode of that name exists in the callgraph), or
     * a Vector (if two or more CallGraphNodes of that name exist in
     * the callgraph).
     */
    Hashtable nodesByName;

    /**
     * The root of all call records. This root is artificially created.
     * It's the parent of all the parent-less CallGraphNode read from
     * a profile.
     */
    CallGraphNode root;

    /**
     * Number of CallGraphNodes in this profile that has one or more children,
     * including this.root.
     */
    int numParents;

    /**
     * Number of graph files to parse.
     */
    int numFiles;

    /**
     * Duration (in msec) of the period when the profile data was collected.
     */
    double period;

    Profile(String[] files) throws IOException {
        nodes = new Hashtable();
        nodesByName = new Hashtable();
        root = createTopNode(null, -1, "root");
        numParents = 0;
        numFiles = files.length;

        if (isMultifile()) {
            for (int i = 0; i < numFiles; i++) {
                readInputFile(new FileParserContext(files[i], i + 1, this));
            }
        } else {
            readInputFile(new FileParserContext(files[0], 0, this));
        }

        createCallGraph();

        if (numParents > 0) {
            root.endTime = period;
            root.computeSummary();
        }

        root.computePercentage();
    }

    boolean isMultifile() {
        return numFiles > 1;
    }
    
    CallGraphNode createTopNode(CallGraphNode parent, int index, String name) {
        CallGraphNode r = new CallGraphNode(parent);

        r.parent = parent;
        r.parentIdx = parent == null ? null : parent.index;
        r.index = new Integer(index);
        r.name = name;

        nodes.put(r.index, r);

        addToIndexByName(r);
        return r;
    }

    void printRecord(CallGraphNode cgNode, String prefix) {
        if (cgNode.parent != null) {
            System.out.print(prefix);
            System.out.print(cgNode.name);
            System.out.print(", D=" + cgNode.depth);
            System.out.print(", K=" + cgNode.count);
            System.out.print(", C=" + cgNode.kidsCycles);
            System.out.print(", M=" + cgNode.kidsMsec);
            System.out.println();
            prefix += " ";
        }
        if (cgNode.children != null) {
            Vector v = cgNode.children;
            for (int i=0; i<v.size(); i++) {
                printRecord((CallGraphNode)v.elementAt(i), prefix);
            }
        }
    }


    /**
     * Read the entire content of an input file
     */
    void readInputFile(FileParserContext ctx) throws IOException {
        FileReader fr = new FileReader(ctx.file);
        BufferedReader reader = new BufferedReader(fr);
        String line;

        /* Skip the first line. It's for human consumption only */
        line = reader.readLine();
        //line = reader.readLine(); /* need revisit */

        // Read all lines in the input file
        //
        while ((line = reader.readLine()) != null) {
            readInputLine(line, ctx);
        }
    }

    void readInputLine(String line, FileParserContext ctx) {
        StringTokenizer st = new StringTokenizer(line);
        if (st.countTokens() != 15) {
            return;
        }

        // Read the raw content of the input line into the CallGraphNode
        try {
            CallGraphNode cgNode;
            int index = Integer.parseInt(st.nextToken());
            if (index == -1) {
                // We also have a predefined root node in our source file -
                // so modify the existing root node
                cgNode = ctx.root;
                st.nextToken(); // parent token is meaningless for the root
            } else {
                cgNode = new CallGraphNode(ctx.root);
                cgNode.index = new Integer(ctx.indexOffset + index);
                cgNode.parentIdx = new Integer(ctx.indexOffset +
                    Integer.parseInt(st.nextToken()));
            }

            cgNode.depth = Integer.parseInt(st.nextToken());
            cgNode.name = st.nextToken();
            cgNode.count = Integer.parseInt(st.nextToken());

            cgNode.onlyCycles = Long.parseLong(st.nextToken());
            cgNode.onlyMsec   = Double.parseDouble(st.nextToken());
            cgNode.onlyPerc   = Double.parseDouble(st.nextToken());

            if (index != -1) {
                // kidsCylcles is calculated automatically for the root node
                cgNode.kidsCycles = Long.parseLong(st.nextToken());
                cgNode.kidsMsec   = Double.parseDouble(st.nextToken());
                cgNode.kidsPerc   = Double.parseDouble(st.nextToken());

                // Timeline information
                cgNode.startTime  = Double.parseDouble(st.nextToken());
                cgNode.endTime    = Double.parseDouble(st.nextToken());
                if (cgNode.endTime > period) {
                    period = cgNode.endTime;
                }

                // Store new CallGraphNode into the hashtable
                nodes.put(cgNode.index, cgNode);
                // Add it to another index table, searchable by cgNode.name
                addToIndexByName(cgNode);
            }
        } catch (NumberFormatException e) {
            // IMPL_NOTE: warning
        }
    }

    void createCallGraph() {
        // Now all the CallGraphNodes are created. We're ready to
        // construct the call graph
        for (Enumeration e = nodes.elements(); e.hasMoreElements() ;) {
            CallGraphNode cgNode = (CallGraphNode)e.nextElement();

            if (cgNode.parentIdx == null) {
                // This is the root record. Do nothing
            } else {
                CallGraphNode parent;
                parent = (CallGraphNode)nodes.get(cgNode.parentIdx);
                if (parent == null) {
                    System.out.println("WARNING: no parent found for element " + 
                                       cgNode);
                    parent = root;
                }

                if (parent.children == null) {
                    parent.children = new Vector();
                    numParents ++;
                }
                parent.children.addElement(cgNode);
                cgNode.parent = parent;
            }
        }
    }

    /**
     * Add this cgNode to the index of all CallGraphNodes by its name.
     * This index is maintained by the hashtable <b>nodesByName</b>
     */
    void addToIndexByName(CallGraphNode cgNode) {
        Object obj = nodesByName.get(cgNode.name);

        //System.out.println(cgNode.name + "=" + obj);
        if (obj == null) {
            // Not in the table yet, store the cgNode itself
            nodesByName.put(cgNode.name, cgNode);
        }
        else if (obj instanceof Vector) {
            // Already has two more more cgNodes of the same name, append
            // to the list
            ((Vector)obj).addElement(cgNode);
        }
        else {
            // We've seen the second cgNode of the same name. Store both
            // cgNodes inside a Vector and put that Vector in the hashtable.
            //
            // By doing this, we only create Vector for those method names
            // that correspond to more than one CallGraphNode
            Vector v = new Vector(2);
            v.addElement(obj);
            v.addElement(cgNode);
            nodesByName.put(cgNode.name, v);
        }
    }
}

class FileParserContext {
    /**
     * Name of the file being parsed.
     */
    String file;
    
    /**
     * indexOffset is added to every node index found in a source file.
     * This helps to avoid collisions between equal indices in different
     * graph files.
     * indexOffset is defined as fileNo * MAX_INDEX
     * where MAX_INDEX is large enough to ensure that
     * every index in a source file is less than MAX_INDEX.
     */
    int indexOffset;

    /**
     * The root node for all records of the file being parsed.
     */
    CallGraphNode root;

    static final int MAX_INDEX = 10000000;

    FileParserContext(String fileName, int fileNo, Profile prof) {
        file = fileName;
        indexOffset = fileNo * MAX_INDEX;
        if (fileNo == 0) {
            // do not create additional level when parsing
            // exactly one source file;
            // all elements are attached directly to the "root"
            root = prof.root;
        } else {
            // create an auxiliary node named <fileName> which will be
            // the virtual root for all records in current file
            root = prof.createTopNode(prof.root, indexOffset - 1, fileName);
        }
    }
}

class Filter {
    boolean prefix;
    String substr;
    double startTime;
    double endTime;

    Filter(String text, double time) {
        if (text != null) {
            substr = text.trim();
            if (substr.equals("")) {
                substr = null;
            } else if (prefix = substr.charAt(0) == '^') {
                substr = substr.substring(1);
            }
        }
        if (time >= 0.0) {
            startTime = time;
            endTime = time;
        } else {
            startTime = Double.MAX_VALUE;
            endTime = -1.0;
        }
    }

    boolean isEmpty() {
        return substr == null && endTime < 0.0;
    }
    
    boolean recordMatches(CallRecord r) {
        if (substr != null) {
            String name = r.name;
            if (name == null) {
                return false;
            }
            int n = name.indexOf(substr);
            if (n < 0 || (prefix && n != 0)) {
                return false;
            }
        }
        return r.startTime <= startTime && r.endTime >= endTime;
    }

    public boolean equals(Object o) {
        if (o == null) {
            return isEmpty();
        }
        if (!(o instanceof Filter)) {
            return false;
        }
        Filter f = (Filter) o;
        if (substr == null) {
            if (f.substr != null) {
                return false;
            }
        } else {
            if (!substr.equals(f.substr) || prefix != f.prefix) {
                return false;
            }
        }
        return startTime == f.startTime && endTime == f.endTime;
    }

}

class CallRecord {
    final static int DEPTH         = 0;
    final static int PARENT        = 1;
    final static int NAME          = 2;
    final static int COUNT         = 3;
    final static int ONLY_CYCLES   = 4;
    final static int AVG_CYCLES    = 5;
    final static int ONLY_MSEC     = 6;
    final static int ONLY_REL_PERC = 7;
    final static int ONLY_PERC     = 8;
    final static int KIDS_CYCLES   = 9;
    final static int KIDS_MSEC     = 10;
    final static int KIDS_REL_PERC = 11;
    final static int KIDS_PERC     = 12;
    final static int START_TIME    = 13;
    final static int END_TIME      = 14;

    final static int NUM_FIELDS    = 15;

    String name;
    int count;
    long onlyCycles;
    long kidsCycles;
    double onlyMsec;
    double kidsMsec;
    double onlyPerc;
    double kidsPerc;
    double startTime;
    double endTime;

    // The topmost node that the record belongs to:
    // <root> for single-file profiles or <fileName> for multifile profiles.
    // It is used to calculate the percentage of the record via
    // dividing its onlyCycles or kidsCycles by owner.kidsCycles
    CallRecord owner;

    // If this record represents a delta record, deltaBase points to
    // the record which was compared to, otherwise deltaBase is null
    CallRecord deltaBase;

    CallRecord(CallRecord owner) {
        this.owner = owner == null ? this : owner;
    }

    void add(CallRecord other, boolean addKids) {
        if (other.startTime < this.startTime || this.count == 0) {
            this.startTime = other.startTime;
        }
        if (other.endTime > this.endTime) {
            this.endTime = other.endTime;
        }

        this.count       += other.count;

        this.onlyCycles  += other.onlyCycles;
        this.onlyMsec    += other.onlyMsec;
        this.onlyPerc    += other.onlyPerc;

        if (addKids) {
            this.kidsCycles  += other.kidsCycles;
            this.kidsMsec    += other.kidsMsec;
            this.kidsPerc    += other.kidsPerc;
        }
    }

    void setDelta(CallRecord base) {
        if (deltaBase == null) {
            count      -= base.count;
            onlyCycles -= base.onlyCycles;
            onlyMsec   -= base.onlyMsec;
            onlyPerc   -= base.onlyPerc;
            kidsCycles -= base.kidsCycles;
            kidsMsec   -= base.kidsMsec;
            kidsPerc   -= base.kidsPerc;
            startTime  -= base.startTime;
            endTime    -= base.endTime;
            deltaBase   = base;
        }
    }

    void clearDelta() {
        if (deltaBase != null) {
            count      += deltaBase.count;
            onlyCycles += deltaBase.onlyCycles;
            onlyMsec   += deltaBase.onlyMsec;
            onlyPerc   += deltaBase.onlyPerc;
            kidsCycles += deltaBase.kidsCycles;
            kidsMsec   += deltaBase.kidsMsec;
            kidsPerc   += deltaBase.kidsPerc;
            startTime  += deltaBase.startTime;
            endTime    += deltaBase.endTime;
            deltaBase   = null;
        }
    }

    int depth;

    long getAvgOnlyCycles() {
        if (this.count <= 0) {
            return 0;
        } else {
            return this.onlyCycles / this.count;
        }
    }

    void computePercentage() {
        if (owner.kidsCycles == 0) {
            onlyPerc = 0;
            kidsPerc = 0;
        } else {
            onlyPerc = (double)onlyCycles / owner.kidsCycles;
            kidsPerc = (double)kidsCycles / owner.kidsCycles;
        }
    }
}

/**
 * AveragedCallRecord is used to support non-trivial merging of similar
 * records from different graph files like geometric averaging.
 */
class AveragedCallRecord extends CallRecord {
    static final int ARITHMETIC_MEAN = 0;
    static final int GEOMETRIC_MEAN  = 1;

    static int averagingMethod = ARITHMETIC_MEAN;

    int multipliers;
    double onlyPercProduct;
    double kidsPercProduct;

    AveragedCallRecord(CallRecord owner) {
        super(owner);
        multipliers = 0;
        onlyPercProduct = 1;
        kidsPercProduct = 1;
    }

    void add(CallRecord other, boolean addKids) {
        super.add(other, addKids);
        multipliers++;
        if (owner != null && other.owner.kidsCycles != 0) {
            onlyPercProduct *= (double)other.onlyCycles / other.owner.kidsCycles;
            kidsPercProduct *= (double)other.kidsCycles / other.owner.kidsCycles;
        }
    }

    void computePercentage() {
        if (averagingMethod == ARITHMETIC_MEAN) {
            super.computePercentage();
            return;
        }
        if (multipliers == 0) {
            onlyPerc = 0;
            kidsPerc = 0;
        } else {
            double power = 1 / (double)multipliers;
            onlyPerc = Math.pow(onlyPercProduct, power);
            kidsPerc = Math.pow(kidsPercProduct, power);
        }
    }
}

class CallGraphNode extends CallRecord {
    /**
     * The parent of this CallGraphNode. The parent/child relationship is
     * defined as: <pre>
     *   if methodA calls methodB, and methodB calls methodC
     * then
     *   CallGraphNode (methodA, methodB) is the parent of
     *   CallGraphNode (methodB, methodC)
     * </pre>
     */
    CallGraphNode parent;

    /**
     * A list of all CallGraphNodes whose parent is this CallGraphNode.
     * Is null of this CallGraphNode has no children.
     */
    Vector children;

    /**
     * Is null iff this CallGraphNode has no children.
     */
    CallRecord summary[];

    Integer index;
    Integer parentIdx;

    CallGraphNode(CallRecord owner) {
        super(owner);
    }
    
    /**
     * Compute the summary of each CallGraphNodes that has at least one child.
     */
    void computeSummary() {
        // (1) Merge the summary of all the children. We use a hashtable
        //     to store the summary at this stage for faster searches.

        Vector v = children;
        Hashtable table = new Hashtable();
        int i;

        /* Create myself */
        CallRecord myself = getSummaryRecord(table, this.name);
        myself.add(this, true);

        for (i=0; i<v.size(); i++) {
            CallGraphNode child = (CallGraphNode)v.elementAt(i);
            if (child.children != null) {
                child.computeSummary();
                for (int j=child.summary.length-1; j>=0; j--) {
                    CallRecord chRec = child.summary[j];
                    
                    CallRecord myRec = getSummaryRecord(table, chRec.name);
                    if (!chRec.name.equals(this.name)) {
                        myRec.add(chRec, true);
                    } else {
                        myRec.add(chRec, false);
                    }
                }
            } else {
                CallRecord myRec = getSummaryRecord(table, child.name);
                if (!child.name.equals(this.name)) {
                    myRec.add(child, true);
                } else {
                    myRec.add(child, false);
                }
            }
        }

        // (2) convert the summary from a hashtable to a vector for
        //     better size (use an array instead??)

        summary = new CallRecord[table.size()];
        i = 0;
        for (Enumeration e = table.elements(); e.hasMoreElements() ;) {
            CallRecord rec = (CallRecord)e.nextElement();
            summary[i++] = rec;
        }

        if (isTopNode()) {
            for (i=0; i<summary.length; i++) {
                CallRecord rec = summary[i];
                myself.kidsCycles  += rec.onlyCycles;
                myself.kidsMsec    += rec.onlyMsec;
                myself.kidsPerc    += rec.onlyPerc;
            }
            this.kidsCycles = myself.kidsCycles;
            this.kidsMsec   = myself.kidsMsec;
            this.kidsPerc   = myself.kidsPerc;
        }
    }

    boolean isTopNode() {
        // This node is global root or virtual file root
        // if and only if its index == -1 (mod MAX_INDEX)
        return (index.intValue() + 1) % FileParserContext.MAX_INDEX == 0;
    }

    void computePercentage() {
        // Compute percentage of this node, all of its subnodes
        // and summary records
        super.computePercentage();
        if (children != null) {
            for (int i = 0; i < children.size(); i++) {
                ((CallGraphNode)children.elementAt(i)).computePercentage();
            }
        }
        if (summary != null) {
            for (int i = 0; i < summary.length; i++) {
                summary[i].computePercentage();
            }
        }
    }

    CallGraphNode findChildByName(String nameToFind) {
        for (int i = 0; i < children.size(); i++) {
            CallGraphNode child = (CallGraphNode)children.elementAt(i);
            if (child.name.equals(nameToFind)) {
                return child;
            }
        }
        return null;
    }

    CallRecord findSummaryByName(String nameToFind) {
        for (int i = 0; i < summary.length; i++) {
            if (summary[i].name.equals(nameToFind)) {
                return summary[i];
            }
        }
        return null;
    }

    void setDelta(CallGraphNode base) {
        super.setDelta(base);

        if (children != null && base.children != null) {
            for (int i = 0; i < children.size(); i++) {
                CallGraphNode child = (CallGraphNode)children.elementAt(i);
                CallGraphNode prototype = base.findChildByName(child.name);
                if (prototype != null) {
                    child.setDelta(prototype);
                }
            }
        }
        
        if (summary != null && base.summary != null) {
            for (int i = 0; i < summary.length; i++) {
                CallRecord prototype = base.findSummaryByName(summary[i].name);
                if (prototype != null) {
                    summary[i].setDelta(prototype);
                }
            }
        }
    }

    void clearDelta() {
        super.clearDelta();
        if (children != null) {
            for (int i = 0; i < children.size(); i++) {
                ((CallGraphNode)children.elementAt(i)).clearDelta();
            }
        }
        if (summary != null) {
            for (int i = 0; i < summary.length; i++) {
                summary[i].clearDelta();
            }
        }
    }

    CallRecord getSummaryRecord(Hashtable table, String name) {
        CallRecord rec = (CallRecord)table.get(name);
        if (rec == null) {
            CallRecord owner = isTopNode() ? this : this.owner;
            // Non-trivial averaging is currently supported for top-level
            // summary records except profile.root (for which name == this.name)
            if (parent == null && name != this.name) {
              rec = new AveragedCallRecord(owner);
            } else {
              rec = new CallRecord(owner);
            }
            rec.name = name;
            table.put(name, rec);
        }

        return rec;
    }

    public String toString() {
        if (parent == null) {
            return "-" + name;
        } else {
            return parent.name + "-" + name;
        }
    }
}

class MergedCallGraphNode extends CallGraphNode {
    MergedCallGraphNode(CallGraphNode src, int levels) {
        super(src.owner);
        if (levels > 0 && src.parent != null) {
            this.parent = new MergedCallGraphNode(src.parent, levels-1);
        } else {
            this.parent = new CallGraphNode(src.owner);
            this.parent.name = "....";
        }
        this.name = src.name;
        this.add(src, true);
    }

    void add(CallGraphNode other, boolean addKids, int levels) {
        if (levels > 0) {
            MergedCallGraphNode myParent = (MergedCallGraphNode)this.parent;
            CallGraphNode otherParent = other.parent;
            if (myParent != null && other.parent != null) {
                myParent.add(otherParent, addKids, levels-1);
            }
        }
        super.add(other, addKids);
    }
}

class MergedCallRecord extends CallRecord {
    MergedCallRecord(CallRecord owner) {
        super(owner);
    }
}

abstract class BaseTableModel extends AbstractTableModel implements Comparator {
    JTable table;
    ProfView viewer;

    BaseTableModel(ProfView viewer) {
        this.viewer = viewer;
    }

    String names[] = {
        "Depth", "Parent", "Name", "Count",
        "Cycles", "Avg cycles", "Msec", "Rel %", "%",
        "Cycles_k", "Msec_k", "Rel %_k", "%_k",
        "Start", "End"
    };

    boolean colVisible[] = new boolean[CallRecord.NUM_FIELDS];

    void setOwner(JTable table) {
        this.table = table;
        setTableColumnWidths();
        addMouseListenerToHeaderInTable();
    }

    void setColumnVisible(int colName, boolean visible) {
        colVisible[colName] = visible;
    }

    /**
     * Convert the visible column (seen by user on the screen) to the
     * "real column", which is CallRecord.NAME, CallRecord.COUNT, etc.
     */
    int toRealCol(int visCol) {
        for (int i=0; i<colVisible.length; i++) {
            if (colVisible[i]) {
                if (visCol == 0) {
                    return i;
                } else {
                    visCol --;
                }
            }
        }
        return 0; // shouldn't be here!
    }

    abstract CallRecord[] getDataArray();
    abstract void setDataArray(CallRecord[] newArray);

    /**
     * Returns true if the contents of the data array are CallGraphNode
     * objects.
     */
    abstract boolean isCallGraphNode();

    CallRecord getRecordByRow(int row) {
        return getDataArray()[row];
    }

    /**
     * Implements AbstractTableModel.getColumnCount().
     */
    public int getColumnCount() {
        int n = 0;
        for (int i=0; i<colVisible.length; i++) {
            if (colVisible[i]) {
                n ++;
            }
        }
        return n;
    }

    /**
     * Implements AbstractTableModel.getColumnName().
     */
    public String getColumnName(int visCol) {
        String name = names[toRealCol(visCol)];
        return name;
    }

    /**
     * Implements AbstractTableModel.getRowCount().
     */
    public int getRowCount() {
        return getDataArray().length;
    }

    /**
     * Implements AbstractTableModel.getValueAt().
     *
     * Get the object to display at (row, visCol) in the table.
     */
    public Object getValueAt(int row, int visCol) {
        CallRecord[] dataArray = getDataArray();
        CallRecord rec = dataArray[row];

        switch (toRealCol(visCol)) {
        case CallRecord.PARENT:
            if (isCallGraphNode()) {
                // IMPL_NOTE: change to use (rec instanceof CallGraphNode) instead
                CallGraphNode parent = ((CallGraphNode)rec).parent;
                if (parent == null) {
                    return ".";
                } else {
                    return parent.name;
                }
            } else {
                return "??";
            }
        case CallRecord.NAME:
            return rec.name;
        case CallRecord.COUNT:
            return new Integer(rec.count);
        case CallRecord.DEPTH:
            return new Integer(rec.depth);
        case CallRecord.ONLY_CYCLES:
            return new Long(rec.onlyCycles);
        case CallRecord.AVG_CYCLES:
            return new Long(rec.getAvgOnlyCycles());
        case CallRecord.ONLY_MSEC:
            return new Double(rec.onlyMsec);
        case CallRecord.ONLY_REL_PERC:
            return getPercObject((double)rec.onlyCycles /
                                 viewer.currentTreeNode.cgNode.kidsCycles);
        case CallRecord.ONLY_PERC:
            return getPercObject(rec.onlyPerc);
        case CallRecord.KIDS_CYCLES:
            return new Long(rec.kidsCycles);
        case CallRecord.KIDS_MSEC:
            return new Double(rec.kidsMsec);
        case CallRecord.KIDS_REL_PERC:
            return getPercObject((double)rec.kidsCycles /
                                 viewer.currentTreeNode.cgNode.kidsCycles);
        case CallRecord.START_TIME:
            return new Double(rec.startTime);
        case CallRecord.END_TIME:
            return new Double(rec.endTime);
        case CallRecord.KIDS_PERC:
        default:
            return getPercObject(rec.kidsPerc);
        }
    }

    /**
     * Get the class of the object at the given visible column.
     */
    public Class getColumnClass(int visCol) {
        switch (toRealCol(visCol)) {
        case CallRecord.PARENT:
        case CallRecord.NAME:
            return String.class;
        case CallRecord.COUNT:
        case CallRecord.DEPTH:
            return Integer.class;
        case CallRecord.ONLY_CYCLES:
        case CallRecord.KIDS_CYCLES:
        case CallRecord.AVG_CYCLES:
            return Long.class;
        case CallRecord.ONLY_PERC:
        case CallRecord.KIDS_PERC:
        case CallRecord.ONLY_REL_PERC:
        case CallRecord.KIDS_REL_PERC:
            return String.class;
        case CallRecord.ONLY_MSEC:
        case CallRecord.KIDS_MSEC:
        case CallRecord.START_TIME:
        case CallRecord.END_TIME:
        default:
            return Double.class;
        }
    }

    //----------------------------------------------------------------------
    //
    // Table sorting
    //
    //----------------------------------------------------------------------

    void addMouseListenerToHeaderInTable() { 
        table.setColumnSelectionAllowed(false); 
        MouseAdapter listMouseListener = new MouseAdapter() {
            public void mouseClicked(MouseEvent e) {
                TableColumnModel columnModel = table.getColumnModel();
                int viewColumn = columnModel.getColumnIndexAtX(e.getX()); 
                int column = table.convertColumnIndexToModel(viewColumn); 
                if (e.getClickCount() == 1 && column != -1) {
                    setSortColumn(column);
                    sort();
                    updateTable(false);
                }
            }
        };
        JTableHeader th = table.getTableHeader(); 
        th.addMouseListener(listMouseListener); 
    }

    /**
     * Implements Comparator.compare
     */
    public int compare(Object o1, Object o2) {
        CallRecord rec1;
        CallRecord rec2;

        if (sortAscending) {
            rec1 = (CallRecord)o1;
            rec2 = (CallRecord)o2;
        } else {
            rec1 = (CallRecord)o2;
            rec2 = (CallRecord)o1;
        }

        switch (sortColumn) {
        case CallRecord.PARENT:
            if (isCallGraphNode()) {
                CallGraphNode parent1 = ((CallGraphNode)rec1).parent;
                CallGraphNode parent2 = ((CallGraphNode)rec2).parent;

                if (parent1 == null) {
                    return -1;
                } else if (parent2 == null) {
                    return 1;
                } else {
                    return parent1.name.compareTo(parent2.name);
                }
            } else {
                return 0;
            }
        case CallRecord.NAME:
            return rec1.name.compareTo(rec2.name);
        case CallRecord.COUNT:
            if (rec1.count == rec2.count) return 0;
            if (rec1.count > rec2.count) return 1;
            if (rec1.count < rec2.count) return -1;
        case CallRecord.DEPTH:
            return rec1.depth - rec2.depth;
        case CallRecord.ONLY_PERC:
            if (rec1.onlyPerc == rec2.onlyPerc) return 0;
            if (rec1.onlyPerc > rec2.onlyPerc) return 1;
            if (rec1.onlyPerc < rec2.onlyPerc) return -1;
        case CallRecord.ONLY_CYCLES:
        case CallRecord.ONLY_MSEC:
        case CallRecord.ONLY_REL_PERC:
            if (rec1.onlyCycles == rec2.onlyCycles) return 0;
            if (rec1.onlyCycles > rec2.onlyCycles) return 1;
            if (rec1.onlyCycles < rec2.onlyCycles) return -1;
        case CallRecord.AVG_CYCLES:
            long rec1_avgCycles = rec1.getAvgOnlyCycles();
            long rec2_avgCycles = rec2.getAvgOnlyCycles();
            if (rec1_avgCycles == rec2_avgCycles) return 0;
            if (rec1_avgCycles > rec2_avgCycles) return 1;
            if (rec1_avgCycles < rec2_avgCycles) return -1;
        case CallRecord.KIDS_PERC:
            if (rec1.kidsPerc == rec2.kidsPerc) return 0;
            if (rec1.kidsPerc > rec2.kidsPerc) return 1;
            if (rec1.kidsPerc < rec2.kidsPerc) return -1;
        case CallRecord.START_TIME:
            if (rec1.startTime == rec2.startTime) return 0;
            if (rec1.startTime > rec2.startTime) return 1;
            if (rec1.startTime < rec2.startTime) return -1;
        case CallRecord.END_TIME:
            if (rec1.endTime == rec2.endTime) return 0;
            if (rec1.endTime > rec2.endTime) return 1;
            if (rec1.endTime < rec2.endTime) return -1;
        case CallRecord.KIDS_CYCLES:
        case CallRecord.KIDS_MSEC:
        case CallRecord.KIDS_REL_PERC:
        default:
            if (rec1.kidsCycles == rec2.kidsCycles) return 0;
            if (rec1.kidsCycles > rec2.kidsCycles) return 1;
            if (rec1.kidsCycles < rec2.kidsCycles) return -1;
            return 0;  // will never happen, of course.
        }
    }

    protected int sortColumn = CallRecord.KIDS_CYCLES;
    protected boolean sortAscending = false;

    /**
     * Set the sorting column. If the same column is set twice consecutively,
     * the sorting direction is reversed.
     */
    public void setSortColumn(int visCol) {
        int column = toRealCol(visCol);
        
        if (sortColumn != column) {
            sortColumn = column;
            if (column == CallRecord.NAME || column == CallRecord.PARENT) {
                sortAscending = true;
            } else {
                sortAscending = false;
            }
        } else {
            sortAscending = !sortAscending;
        }
    }

    /**
     * Sort the data array using the current sortColumn.
     */
    public void sort() {
        CallRecord[] dataArray = getDataArray();
        java.util.List l = Arrays.asList(dataArray);
        Collections.sort(l, this);
        setDataArray((CallRecord[])l.toArray((Object[])dataArray));
    }

    /**
     * Export the current view of the table into a file.
     * IMPL_NOTE: make the file name selectable
     */
    public void export() {
        try {
            CallRecord[] dataArray = getDataArray();
            for (int i=0; i<dataArray.length; i++) {
                CallRecord cr = dataArray[i];
                String name = cr.name;
                System.out.println("Precompile = " + name);
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    //----------------------------------------------------------------------
    //
    // Table Column Setup Frame
    //
    //----------------------------------------------------------------------

    SetupFrame setupFrame;

    void setup() {
        if (setupFrame == null) {
            setupFrame = new SetupFrame();
        }
        setupFrame.setVisible(true);
        setupFrame.toFront();
    }

    class SetupFrame extends JFrame implements ActionListener {
        JCheckBox buttons[];

        SetupFrame() {
            super("setup");

            // Put the check boxes in a column in a panel
            JPanel checkPanel = new JPanel();
            checkPanel.setLayout(new GridLayout(0, 1));

            buttons = new JCheckBox[names.length];
            for (int i=0; i<names.length; i++) {
                buttons[i] = new JCheckBox(names[i]);
                buttons[i].setSelected(colVisible[i]);

                checkPanel.add(buttons[i]);
            }

            JButton apply = new JButton("Apply");
            apply.setActionCommand("apply");
            apply.addActionListener(this);

            checkPanel.add(apply);

            this.getContentPane().add(checkPanel);
            this.pack();
            this.setLocation(300, 100);
            this.setVisible(true);

            //setLayout(new BorderLayout());
            //add(checkPanel, BorderLayout.WEST);
            //add(pictureLabel, BorderLayout.CENTER);
            //setBorder(BorderFactory.createEmptyBorder(20,20,20,20));
        }

        public void actionPerformed(java.awt.event.ActionEvent e) {
            if (e.getActionCommand().equals("apply")) {
                boolean changed = false;
                for (int i=0; i<buttons.length; i++) {
                    if (colVisible[i] != buttons[i].isSelected()) {
                        changed = true ;
                        colVisible[i] = buttons[i].isSelected();
                    }
                }
                if (changed) {
                    updateTable(true);
                }
                setVisible(false);
            }
        }
    }

    void setTableColumnWidths() {
        if (this.table == null) {
            return;
        }

        TableColumnModel colModel = table.getColumnModel();

        for (int realCol=0, visCol=0; realCol<names.length; realCol++) {
            if (!colVisible[realCol]) {
                continue;
            }
            int prefWidth;
            switch (realCol) {
            case CallRecord.PARENT:
            case CallRecord.NAME:
                prefWidth = 280;
                break;
            case CallRecord.COUNT:
            case CallRecord.ONLY_CYCLES:
            case CallRecord.AVG_CYCLES:
            case CallRecord.KIDS_CYCLES:
                prefWidth = 70;
                break;
            case CallRecord.DEPTH:
            case CallRecord.ONLY_MSEC:
            case CallRecord.ONLY_PERC:
            case CallRecord.ONLY_REL_PERC:
                prefWidth = 40;
                break;
            case CallRecord.KIDS_MSEC:
            case CallRecord.KIDS_PERC:
            case CallRecord.KIDS_REL_PERC:
            case CallRecord.START_TIME:
            case CallRecord.END_TIME:
            default:
                prefWidth = 50;
            }

            colModel.getColumn(visCol).setPreferredWidth(prefWidth);
            ++ visCol;
        }
    }

    public void updateTable(boolean colsChanged) {
        if (colsChanged) {
            // If the columns has changed, we must do the following
            // to force the table to refresh the column headers.
            table.setModel(new DefaultTableModel());
            table.updateUI();

            table.setModel(this);
            setTableColumnWidths();
        } else {
            table.setModel(this);
        }
        table.updateUI();
    }

    //----------------------------------------------------------------------
    //
    // Percentage viewing
    //
    //----------------------------------------------------------------------

    Object getPercObject(double val) {
        int ival = Math.abs((int)(val * 10000));
        int i = ival / 100;
        int f = ival % 100;

        StringBuffer sbuf = new StringBuffer();
        if (i < 100) {
            sbuf.append(" ");
            if (i < 10) {
                sbuf.append(" ");
            }
        }
        sbuf.append(val < 0 ? "-" : " ");

        sbuf.append(i);
        sbuf.append(".");
        if (f < 10) {
            sbuf.append("0");
        }
        sbuf.append(f);

        return sbuf.toString();
    }
}


class MainTableModel extends BaseTableModel {
    MainTableModel(ProfView viewer) {
        super(viewer);
        setColumnVisible(CallRecord.PARENT,        false);
        setColumnVisible(CallRecord.NAME,          true);
        setColumnVisible(CallRecord.COUNT,         true);
        setColumnVisible(CallRecord.DEPTH,         false);
        setColumnVisible(CallRecord.AVG_CYCLES,    true);
        setColumnVisible(CallRecord.ONLY_CYCLES,   true);
        setColumnVisible(CallRecord.ONLY_MSEC,     true);
        setColumnVisible(CallRecord.ONLY_REL_PERC, false);
        setColumnVisible(CallRecord.ONLY_PERC,     true);
        setColumnVisible(CallRecord.KIDS_CYCLES,   true);
        setColumnVisible(CallRecord.KIDS_MSEC,     true);
        setColumnVisible(CallRecord.KIDS_REL_PERC, false);
        setColumnVisible(CallRecord.KIDS_PERC,     true);
        setColumnVisible(CallRecord.START_TIME,    false);
        setColumnVisible(CallRecord.END_TIME,      false);
    }

    boolean isCallGraphNode() {
        return false;
    }

    CallGraphNode cgNode;

    CallRecord noRecord[] = new CallRecord[0];
    CallRecord singleRecord[] = new CallRecord[1];
    CallRecord children[];
    boolean showChildrenOnly;

    void setDataArray(CallRecord dataArray[]) {
        if (cgNode != null && cgNode.summary != null &&
            dataArray != lastFilterResult) {
            if (showChildrenOnly) {
                children = dataArray;
            } else {
                cgNode.summary = dataArray;
            }
        }
    }

    /**
     * Restrict the TableModel to display only the CallRecords that are
     * reachable (directly or indirectly) from this cgNode.
     */
    void setCurrentDataSet(CallGraphNode cgNode) {
        this.cgNode = cgNode;
        this.children = (cgNode == null || cgNode.children == null) ? null :
                        (CallRecord[]) cgNode.children.toArray(noRecord);
        sort();
    }

    Filter lastFilter;
    CallRecord lastFilterSrc[];
    CallRecord lastFilterResult[];

    CallRecord[] getDataArray() {
        CallRecord array[];

        if (cgNode == null) {
            return noRecord;
        } else if (cgNode.summary == null) {
            singleRecord[0] = cgNode;
            array = singleRecord;
        } else if (showChildrenOnly) {
            array = children;
        } else {
            array = cgNode.summary;
        }

        if (viewer.filter != null) {
            array = filterData(array, viewer.filter);
        }
        return array;
    }

    
    CallRecord[] filterData(CallRecord src[], Filter filter) {
        if (src == lastFilterSrc && filter.equals(lastFilter)) {
            return lastFilterResult;
        }

        lastFilter = filter;
        lastFilterSrc = src;

        Vector v = new Vector();
        for (int i = 0; i < src.length; i++) {
            CallRecord r = src[i];
            if (filter.recordMatches(r)) {
                v.addElement(r);
            }
        }

        CallRecord result[];
        if (src.length != v.size()) {
            if (v.size() == 0) {
                result = noRecord;
            } else {
                result = new CallRecord[v.size()];
                v.toArray((Object[])result);
            }
        } else {
            result = src;
        }

        lastFilterResult = result;
        return result;
    }
}

class GotoFrame extends JFrame implements ListSelectionListener,
                                          ActionListener
{
    Profile profile;
    JTable siteTable, stackTable;
    SiteTableModel siteTableModel;
    StackTableModel stackTableModel;
    ProfView viewer;
    JFormattedTextField levelText;

    GotoFrame(ProfView viewer, Profile profile) {
        this.viewer = viewer;
        this.profile = profile;

        Component sitePanel = createSitePanel();
        Component stackPanel = createStackPanel();

        JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
        splitPane.setTopComponent(sitePanel);
        splitPane.setBottomComponent(stackPanel);

        this.getContentPane().add(splitPane, BorderLayout.CENTER);
        this.setLocation(70, 30);
        this.setSize(600, 500);
        this.pack();
    }

    Component createSitePanel() {
        // The table
        siteTableModel = new SiteTableModel(viewer);
        siteTable = new ProfileTable(siteTableModel, this);
        siteTableModel.setOwner(siteTable);

        siteTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        ListSelectionModel rowSM = siteTable.getSelectionModel();
        rowSM.addListSelectionListener(this);

        JScrollPane scrollPane = new JScrollPane(siteTable);
        scrollPane.setMinimumSize(new Dimension(180, 50));
        scrollPane.setPreferredSize(new Dimension(600, 300));

        // The button panel
        JPanel btnPanel = new JPanel();
        btnPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
        btnPanel.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));

        JCheckBox merge = new JCheckBox("Merge callers"); 
        merge.setActionCommand("merge_callers");
        merge.addActionListener(this);

        // Number of levels in merge
        JLabel levelLabel = new JLabel("Levels: ", JLabel.LEFT);
        levelLabel.setAlignmentX(Component.CENTER_ALIGNMENT);

        java.text.NumberFormat numberFormat =
            java.text.NumberFormat.getIntegerInstance();
        NumberFormatter formatter = new NumberFormatter(numberFormat);
        formatter.setMinimum(new Integer(1));
        formatter.setMaximum(new Integer(10000));
        levelText = new JFormattedTextField(formatter);
        levelText.setValue(new Integer(1));
        levelText.setColumns(3);
        levelText.addActionListener(this);

        btnPanel.add(levelLabel);
        btnPanel.add(levelText);
        btnPanel.add(merge);
        
        // put everything together
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(new JLabel("Call Sites"), BorderLayout.NORTH);
        panel.add(scrollPane, BorderLayout.CENTER);
        panel.add(btnPanel, BorderLayout.SOUTH);

        return panel;
    }

    Component createStackPanel() {
        // The table
        stackTableModel = new StackTableModel(viewer);
        stackTable = new ProfileTable(stackTableModel, this);
        stackTableModel.setOwner(stackTable);

        stackTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        //ListSelectionModel rowSM = stackTable.getSelectionModel();
        //rowSM.addListSelectionListener(this);

        JScrollPane scrollPane = new JScrollPane(stackTable);
        scrollPane.setMinimumSize(new Dimension(180, 50));
        scrollPane.setPreferredSize(new Dimension(600, 300));

        // put everything together
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(new JLabel("Call Stack"), BorderLayout.NORTH);
        panel.add(scrollPane, BorderLayout.CENTER);

        return panel;
    }

    void display(String name) {
        siteTableModel.setCurrentDataSet(name);
        siteTableModel.updateTable(false);
        siteTableModel.sort();

        ListSelectionModel rowSM = siteTable.getSelectionModel();
        rowSM.clearSelection();

        this.setTitle("All occurances of " + name);
    }

    /**
     * Handles Table selection changes. Implements 
     * ListSelectionListener.valueChanged
     */
    public void valueChanged(ListSelectionEvent e) {
        if (e.getValueIsAdjusting()) {
            return;
        }
        
        ListSelectionModel lsm = (ListSelectionModel)e.getSource();
        if (!lsm.isSelectionEmpty()) {
            int selectedRow = lsm.getMinSelectionIndex();
            siteTableRowSelected(selectedRow);
        }
    }

    /**
     * User has selected an item in the site table.
     */
    void siteTableRowSelected(int row) {
        CallGraphNode n = siteTableModel.nodes[row];
        stackTableModel.setCurrentDataSet(n);
        stackTableModel.updateTable(false);

        ListSelectionModel lsm = stackTable.getSelectionModel();
        lsm.clearSelection();
        lsm.addSelectionInterval(0, 0);
    }

    class SiteTableModel extends BaseTableModel {
        String currentName = null;
        boolean merged = false;

        SiteTableModel(ProfView viewer) {
            super(viewer);
            setColumnVisible(CallRecord.PARENT,        true);
            setColumnVisible(CallRecord.NAME,          false);
            setColumnVisible(CallRecord.COUNT,         true);
            setColumnVisible(CallRecord.DEPTH,         true);
            setColumnVisible(CallRecord.ONLY_CYCLES,   false);
            setColumnVisible(CallRecord.ONLY_MSEC,     true);
            setColumnVisible(CallRecord.ONLY_REL_PERC, false);
            setColumnVisible(CallRecord.ONLY_PERC,     true);
            setColumnVisible(CallRecord.KIDS_CYCLES,   false);
            setColumnVisible(CallRecord.KIDS_MSEC,     true);
            setColumnVisible(CallRecord.KIDS_REL_PERC, false);
            setColumnVisible(CallRecord.KIDS_PERC,     true);
            setColumnVisible(CallRecord.START_TIME,    false);
            setColumnVisible(CallRecord.END_TIME,      false);

            setSortColumn(CallRecord.KIDS_PERC);
        }

        boolean isCallGraphNode() {
            return true;
        }

        /**
         * All CallGraphNode to be displayed in this window
         */
        CallGraphNode nodes[] = new CallGraphNode[0];

        CallRecord[] getDataArray() {
            return nodes;
        }
        void setDataArray(CallRecord dataArray[]) {
            nodes = (CallGraphNode[])dataArray;
        }

        /**
         * Restrict this TableModel to display only the CallRecords with
         * the given name.
         */
        void setCurrentDataSet(String name) {
            Object obj = profile.nodesByName.get(name);
            if (obj == null) {
                this.nodes = new CallGraphNode[0];
            }
            else if (obj instanceof CallGraphNode) {
                this.nodes = new CallGraphNode[1];
                this.nodes[0] = (CallGraphNode)obj;
            }
            else {
                Vector v = (Vector)obj;
                if (merged) {
                    v = mergeParents(v);
                }
                this.nodes = new CallGraphNode[v.size()];
                v.toArray((Object[])nodes);
            }
            currentName = name;
        }

        void updateCurrentDataSet(boolean merged) {
            if (currentName != null) {
                this.merged = merged;
                setCurrentDataSet(currentName);
            }
        }

        boolean sameAncestors(CallGraphNode n1, CallGraphNode n2, int levels) {
            for (int i=0; i<levels; i++) {
                String name1 = null, name2 = null;

                if (n1.parent == null && n2.parent == null) {
                    // both n1 and n2 are exactly the same call stack. Why
                    // will this happen??
                    return true;
                }
                if (n1.parent != null) {
                    name1 = n1.parent.name;
                } else {
                    return false;
                }
                if (n2.parent != null) {
                    name2 = n2.parent.name;
                } else {
                    return false;
                }

                if (!name1.equals(name2)) {
                    return false;
                }
                n1 = n1.parent;
                n2 = n2.parent;
            }
            return true;
        }

        /**
         * Create a list of "fake" parent nodes -- if function P may call 
         * function C with different call stacks, create a new node that 
         * combins all those nodes into a single node. This makes it easy
         * to tell how much time is spent in the P->C call, under all
         * circumstances
         */
        Vector mergeParents(Vector nodes) {
            int mergeLevels = 3;
            int i, j;
            Vector mp = new Vector();
            for (i=0; i<nodes.size(); i++) {
                CallGraphNode n1 = (CallGraphNode)nodes.elementAt(i);
                boolean merged = false;
                for (j=0; j<mp.size(); j++) {
                    CallGraphNode n2 = (CallGraphNode)mp.elementAt(j);
                    if (sameAncestors(n1, n2, mergeLevels)) {
                        if (!(n2 instanceof MergedCallGraphNode)) {
                            n2 = new MergedCallGraphNode(n2, mergeLevels);
                            mp.set(j, n2);
                        }

                        ((MergedCallGraphNode)n2).add(n1, true, mergeLevels);
                        merged = true;
                        break;
                    }
                }
                if (!merged) {
                    mp.addElement(n1);
                }
            }

            return mp;
        }
    }

    class StackTableModel extends BaseTableModel {
        StackTableModel(ProfView viewer) {
            super(viewer);

            setColumnVisible(CallRecord.PARENT,        false);
            setColumnVisible(CallRecord.NAME,          true);
            setColumnVisible(CallRecord.COUNT,         true);
            setColumnVisible(CallRecord.DEPTH,         true);
            setColumnVisible(CallRecord.ONLY_CYCLES,   false);
            setColumnVisible(CallRecord.ONLY_MSEC,     true);
            setColumnVisible(CallRecord.ONLY_REL_PERC, false);
            setColumnVisible(CallRecord.ONLY_PERC,     true);
            setColumnVisible(CallRecord.KIDS_CYCLES,   false);
            setColumnVisible(CallRecord.KIDS_MSEC,     true);
            setColumnVisible(CallRecord.KIDS_REL_PERC, false);
            setColumnVisible(CallRecord.KIDS_PERC,     true);
            setColumnVisible(CallRecord.START_TIME,    false);
            setColumnVisible(CallRecord.END_TIME,      false);

            setSortColumn(CallRecord.DEPTH);
        }

        boolean isCallGraphNode() {
            return true;
        }

        /**
         * All CallGraphNode to be displayed in this window
         */
        CallGraphNode nodes[] = new CallGraphNode[0];

        CallRecord[] getDataArray() {
            return nodes;
        }
        void setDataArray(CallRecord dataArray[]) {
            nodes = (CallGraphNode[])dataArray;
        }

        /**
         * Restrict this TableModel to display only the CallRecords with
         * the given name.
         */
        void setCurrentDataSet(CallGraphNode cgNode) {
            Vector v = new Vector();
            for (;
                 cgNode != null && cgNode.parent != null;
                 cgNode = cgNode.parent) {

                v.addElement(cgNode);
            }

            nodes = new CallGraphNode[v.size()];
            nodes = (CallGraphNode[])v.toArray((Object[])nodes);
        }
    }

    public void actionPerformed(java.awt.event.ActionEvent e) {
        String command = e.getActionCommand();

        if (command.equals("merge_callers")) {
            JCheckBox cb = (JCheckBox)e.getSource();
            siteTableModel.updateCurrentDataSet(cb.isSelected());
            siteTableModel.updateTable(false);
        } 
        else if (command.equals("table_dblclick")) {
            SyncCallGraph((ProfileTable)e.getSource());
        };
    }

    CallGraphNode getNodeFromTable(ProfileTable table, int row) {
        BaseTableModel dataModel = (BaseTableModel)table.getModel();
        return ((CallGraphNode[])dataModel.getDataArray())[row];
    }
    
    void SyncCallGraph(ProfileTable table) {
        ListSelectionModel lsm = table.getSelectionModel();
        if (!lsm.isSelectionEmpty()) {
            int selectedRow = lsm.getMinSelectionIndex();
            if (selectedRow >= 0) {
                CallGraphNode cgNode = getNodeFromTable(table, selectedRow);
                if (table == siteTable) {
                    cgNode = cgNode.parent;
                }
                viewer.gotoGraph(cgNode);
                viewer.frame.toFront();
            }
        }
    }
}

/**
 * This window displays all functions reachable from a function X, regardless
 * of the call stack of X.
 */
class CalleeFrame extends JFrame implements ActionListener
{
    Profile profile;
    JTable calleeTable;
    CalleeTableModel calleeTableModel;
    ProfView viewer;

    CalleeFrame(ProfView viewer, Profile profile) {
        this.viewer = viewer;
        this.profile = profile;

        Component calleePanel = createCalleePanel();
        this.getContentPane().add(calleePanel, BorderLayout.CENTER);
        this.setLocation(30, 80);
        this.setSize(600, 500);
        this.pack();
    }

    Component createCalleePanel() {
        // The table
        calleeTableModel = new CalleeTableModel(viewer);
        calleeTable = new ProfileTable(calleeTableModel, null);
        calleeTableModel.setOwner(calleeTable);
        calleeTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        JScrollPane scrollPane = new JScrollPane(calleeTable);
        scrollPane.setMinimumSize(new Dimension(180, 50));
        scrollPane.setPreferredSize(new Dimension(600, 300));

        // put everything together
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(new JLabel("Callees"), BorderLayout.NORTH);
        panel.add(scrollPane, BorderLayout.CENTER);

        return panel;
    }

    void display(String name) {
        calleeTableModel.setCurrentDataSet(name);
        calleeTableModel.updateTable(false);
        calleeTableModel.sort();

        ListSelectionModel rowSM = calleeTable.getSelectionModel();
        rowSM.clearSelection();

        this.setTitle("All callees of " + name);
    }

    class CalleeTableModel extends BaseTableModel {
        String currentName = null;

        CalleeTableModel(ProfView viewer) {
            super(viewer);
            setColumnVisible(CallRecord.PARENT,        false);
            setColumnVisible(CallRecord.NAME,          true);
            setColumnVisible(CallRecord.COUNT,         true);
            setColumnVisible(CallRecord.DEPTH,         false);
            setColumnVisible(CallRecord.ONLY_CYCLES,   false);
            setColumnVisible(CallRecord.ONLY_MSEC,     true);
            setColumnVisible(CallRecord.ONLY_REL_PERC, false);
            setColumnVisible(CallRecord.ONLY_PERC,     true);
            setColumnVisible(CallRecord.KIDS_CYCLES,   false);
            setColumnVisible(CallRecord.KIDS_MSEC,     true);
            setColumnVisible(CallRecord.KIDS_REL_PERC, false);
            setColumnVisible(CallRecord.KIDS_PERC,     true);
            setColumnVisible(CallRecord.START_TIME,    false);
            setColumnVisible(CallRecord.END_TIME,      false);

            setSortColumn(CallRecord.KIDS_PERC);
        }

        boolean isCallGraphNode() {
            return false;
        }

        /**
         * All CallGraphNode to be displayed in this window
         */
        CallRecord nodes[] = new CallRecord[0];

        CallRecord[] getDataArray() {
            return nodes;
        }
        void setDataArray(CallRecord dataArray[]) {
            nodes = dataArray;
        }

        /**
         * Restrict this TableModel to display only the CallRecords with
         * the given name.
         */
        void setCurrentDataSet(String name) {
            Object obj = profile.nodesByName.get(name);
            if (obj == null) {
                this.nodes = new CallRecord[0];
            } else {
                Hashtable h = new Hashtable();

                if (obj instanceof CallGraphNode) {
                    addToDataSet(h, (CallGraphNode)obj);
                } else {
                    Vector v = (Vector)obj;
                    for (int i=0; i<v.size(); i++) {
                        addToDataSet(h, (CallGraphNode)v.elementAt(i));
                    }
                }

                this.nodes = new CallRecord[h.size()];
                int i=0;
                for (Enumeration e = h.elements();
                     e.hasMoreElements(); i++) {
                    this.nodes[i] = (CallRecord)(e.nextElement());
                }
            }
            currentName = name;
        }

        /**
         * Merge all callees of function X, regardless how we get to X.
         */
        void addToDataSet(Hashtable h, CallGraphNode node) {
            if (node == null || node.summary == null) {
                return;
            }

            CallRecord summary[] = node.summary;
            for (int i=0; i<summary.length; i++) {
                CallRecord newRec = summary[i];
                CallRecord oldRec = (CallRecord)h.get(newRec.name);
                if (oldRec == null) {
                    h.put(newRec.name, newRec);
                } else {
                    if (!(oldRec instanceof MergedCallRecord)) {
                        MergedCallRecord mc = new MergedCallRecord(oldRec.owner);
                        mc.name = oldRec.name;
                        mc.add(oldRec, true);
                        oldRec = mc;
                        h.put(oldRec.name, oldRec);
                    }
                    oldRec.add(newRec, true);
                }
            }
        }
    }

    public void actionPerformed(java.awt.event.ActionEvent e) {
        String command = e.getActionCommand();

        if (command.equals("setup_callee_table")) {
            calleeTableModel.setup();
        } 
    }
}

class MyTreeNode extends DefaultMutableTreeNode {
    CallGraphNode cgNode;
    MyTreeNode(CallGraphNode cgNode) {
        super(cgNode.name + 
              " [" + cgNode.onlyCycles + 
              "/"  + cgNode.kidsCycles + "]");
        this.cgNode = cgNode;
    }
}

class ProfileTable extends JTable implements ActionListener, MouseListener,
                                             TableCellRenderer {
    JPopupMenu popup;
    JRadioButtonMenuItem btnAbsolute, btnRelative, btnBoth, btnUnknown;
    JCheckBoxMenuItem btnTimeLine, btnChildrenOnly;
    ActionListener doubleClickListener;
    FontMetrics fm;
    TableColumnModel tcm;
    boolean showToolTip;
    TableCellRenderer renderer;

    void addMenuItem(String caption, String command) {
      JMenuItem item = popup.add(caption);
      item.setActionCommand(command);
      item.addActionListener(this);
    }

    JRadioButtonMenuItem addRadioMenuItem(ButtonGroup group,
                                          String caption, String command) {
      JRadioButtonMenuItem item = new JRadioButtonMenuItem(caption);
      item.setActionCommand(command);
      item.addActionListener(this);
      group.add(item);
      popup.add(item);
      return item;
    }

    JCheckBoxMenuItem addCheckBoxMenuItem(String caption, String command) {
      JCheckBoxMenuItem item = new JCheckBoxMenuItem(caption);
      item.setActionCommand(command);
      item.addActionListener(this);
      popup.add(item);
      return item;
    }

    int isVisible(int column, int score) {
        return ((BaseTableModel)dataModel).colVisible[column] ? score : 0;
    }
    
    void maybePopup(MouseEvent e) {
        if (e.isPopupTrigger()) {
            switch(isVisible(CallRecord.ONLY_PERC,     1) +
                   isVisible(CallRecord.KIDS_PERC,     2) +
                   isVisible(CallRecord.ONLY_REL_PERC, 4) +
                   isVisible(CallRecord.KIDS_REL_PERC, 8)) {
            case 3:
                btnAbsolute.setSelected(true);
                break;
            case 12:
                btnRelative.setSelected(true);
                break;
            case 15:
                btnBoth.setSelected(true);
                break;
            default:
                btnUnknown.setSelected(true);
            }
            popup.show(e.getComponent(), e.getX(), e.getY());
        }
    }

    ProfileTable(AbstractTableModel model, ActionListener doubleClickListener) {
        super(model);
        fm = getFontMetrics(getFont());
        tcm = getColumnModel();
        this.doubleClickListener = doubleClickListener;
        addMouseListener(this);

        popup = new JPopupMenu();
        addMenuItem("Columns...", "table_columns");
        ButtonGroup group = new ButtonGroup();
        btnAbsolute = addRadioMenuItem(group, "Absolute percentage", "percents");
        btnRelative = addRadioMenuItem(group, "Relative percentage", "percents");
        btnBoth = addRadioMenuItem(group, "Both", "percents");
        group.add(btnUnknown = new JRadioButtonMenuItem(""));
        btnTimeLine = addCheckBoxMenuItem("Timeline data", "timeline");
        if (model instanceof MainTableModel) {
            btnChildrenOnly = addCheckBoxMenuItem("Show children only", "children");
        }
        popup.addSeparator();
        addMenuItem("Export for precompilation", "table_precompile");
    }

    public TableCellRenderer getCellRenderer(int row, int column) {
        renderer = super.getCellRenderer(row, column);
        return this;
    }
    
    public Component getTableCellRendererComponent(JTable table, Object value,
                                                   boolean isSelected,
                                                   boolean hasFocus,
                                                   int row, int column) {
        Component c = renderer.getTableCellRendererComponent(
            table, value, isSelected, hasFocus, row, column
        );
        CallRecord[] data = ((BaseTableModel)dataModel).getDataArray();
        if (data[row].deltaBase == null) {
            c.setForeground(ProfView.DEFAULT_COLOR);
        } else {
            c.setForeground(ProfView.DELTA_COLOR);
        }
        return c;
    }

    public void actionPerformed(ActionEvent e) {
        String command = e.getActionCommand();
        BaseTableModel tableModel = (BaseTableModel)dataModel;
        if (command.equals("table_columns")) {
            tableModel.setup();
        } else if (command.equals("table_precompile")) {
            tableModel.export();
        } else if (command.equals("percents")) {
            Object source = e.getSource();
            boolean absolute = source == btnAbsolute || source == btnBoth;
            boolean relative = source == btnRelative || source == btnBoth;
            tableModel.setColumnVisible(CallRecord.ONLY_PERC, absolute);
            tableModel.setColumnVisible(CallRecord.KIDS_PERC, absolute);
            tableModel.setColumnVisible(CallRecord.ONLY_REL_PERC, relative);
            tableModel.setColumnVisible(CallRecord.KIDS_REL_PERC, relative);
            tableModel.updateTable(true);
        } else if (command.equals("timeline")) {
            boolean show = btnTimeLine.getState();
            tableModel.setColumnVisible(CallRecord.START_TIME, show);
            tableModel.setColumnVisible(CallRecord.END_TIME, show);
            tableModel.updateTable(true);
        } else if (command.equals("children")) {
            ((MainTableModel)tableModel).showChildrenOnly =
                btnChildrenOnly.getState();
            tableModel.updateTable(false);
        }
    }

    public void mouseClicked(MouseEvent e) {
        Point p = e.getPoint();
        int row = rowAtPoint(p);
        int col = columnAtPoint(p);
        String s = getValueAt(row, col).toString(); 
        Clipboard clp = Toolkit.getDefaultToolkit().getSystemSelection();
        if (clp == null) {
            clp = Toolkit.getDefaultToolkit().getSystemClipboard();
        }
        clp.setContents(new StringSelection(s), null);
        if (e.getClickCount() == 2 && doubleClickListener != null) {
            doubleClickListener.actionPerformed(
                new ActionEvent(this, 0, "table_dblclick", e.getModifiers())
            );
        }
    }

    public void mouseReleased(MouseEvent e) {
        maybePopup(e);
    }
    
    public void mousePressed(MouseEvent e) {
        maybePopup(e);
    }

    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}

    public String getToolTipText(MouseEvent e) {
        Point p = e.getPoint();
        int row = rowAtPoint(p);
        int col = columnAtPoint(p);

        String s = getValueAt(row, col).toString();
        showToolTip = fm.stringWidth(s) + 3 > tcm.getColumn(col).getWidth();
        return showToolTip ? s : null;
    }

    public Point getToolTipLocation(MouseEvent e) {
        if (showToolTip) {
            Point p = e.getPoint();
            int row = rowAtPoint(p);
            int col = columnAtPoint(p);
            return getCellRect(row, col, false).getLocation();
        } else {
            return null;
        }
    }
}

class ProfileTreeCellRenderer extends DefaultTreeCellRenderer {
    ImageIcon iconJavaInt, iconJavaComp;

    static ImageIcon createImageIcon(String path) {
        java.net.URL imgURL = ProfileTreeCellRenderer.class.getResource(path);
        if (imgURL == null) {
            System.out.println("WARNING: Couldn't find resource: " + path);
            return null;
        }
        return new ImageIcon(imgURL);
    }

    ProfileTreeCellRenderer() {
        super();
        iconJavaInt  = createImageIcon("icons/java_int.gif");
        iconJavaComp = createImageIcon("icons/java_comp.gif");
        ImageIcon iconMethod = createImageIcon("icons/method.gif");
        setOpenIcon(iconMethod);
        setClosedIcon(iconMethod);
        setLeafIcon(createImageIcon("icons/method_leaf.gif"));
    }

    public Component getTreeCellRendererComponent(JTree tree, Object value,
                                                  boolean selected,
                                                  boolean expanded,
                                                  boolean leaf, int row,
                                                  boolean hasFocus) {
        super.getTreeCellRendererComponent(
            tree, value, selected, expanded, leaf, row, hasFocus
        );
        CallGraphNode cgNode = ((MyTreeNode)value).cgNode;
        if (cgNode.deltaBase == null) {
            setForeground(ProfView.DEFAULT_COLOR);
        } else {
            setForeground(ProfView.DELTA_COLOR);
        }
        if (cgNode.name.endsWith("<i>")) {
            setIcon(iconJavaInt);
        } else if (cgNode.name.endsWith("<c>")) {
            setIcon(iconJavaComp);
        }
        return this;
    }

}

class ProfileTree extends JTree implements MouseListener {
    boolean showToolTip;
    Rectangle rect;
    int indent;
    JPopupMenu popup;
    
    public ProfileTree(TreeNode root) {
        super(root);
        ProfileTreeCellRenderer renderer = new ProfileTreeCellRenderer();
        indent = renderer.getOpenIcon().getIconWidth();
        setCellRenderer(renderer);
        ToolTipManager.sharedInstance().registerComponent(this);

        popup = new JPopupMenu();
        popup.add("Export to...").setActionCommand("tree_export");
        popup.addSeparator();
        popup.add("Compare to...").setActionCommand("tree_delta");
        popup.add("Restore original values").setActionCommand("tree_restore");
        addMouseListener(this);
    }

    public void addTreeListener(ActionListener listener) {
        MenuElement[] items = popup.getSubElements();
        for (int i = 0; i < items.length; i++) {
            ((JMenuItem)items[i]).addActionListener(listener);
        }
    }

    public void mouseReleased(MouseEvent e) {
        maybePopup(e);
    }
    
    public void mousePressed(MouseEvent e) {
        maybePopup(e);
    }

    public void mouseClicked(MouseEvent e) {}
    public void mouseEntered(MouseEvent e) {}
    public void mouseExited(MouseEvent e) {}

    void maybePopup(MouseEvent e) {
        if (e.isPopupTrigger()) {
            TreePath path = getPathForLocation(e.getX(), e.getY());
            if (path != null) {
                setSelectionPath(path);
                popup.show(e.getComponent(), e.getX(), e.getY());
            }
        }
    }

    public String getToolTipText(MouseEvent e) {
        TreePath path = getPathForLocation(e.getX(), e.getY());
        if (path == null) {
            showToolTip = false;
        } else {
            rect = getPathBounds(path);
            showToolTip = rect.getX() + rect.getWidth() >
                          getVisibleRect().getWidth();
        }
        return showToolTip ? path.getLastPathComponent().toString() : null;
    }

    public Point getToolTipLocation(MouseEvent e) {
        if (showToolTip) {
            Point p = rect.getLocation();
            p.translate(indent, 0);
            return p;
        } else {
            return null;
        }
    }
}

class ProfileWriter {
    CallGraphNode root;
    boolean normalize;
    int rootIndex;

    ProfileWriter(CallGraphNode root, boolean needToNormalize) {
        this.root = root;
        this.normalize = needToNormalize;
        this.rootIndex = root.index.intValue();
    }

    // Normalizes indices of the graph nodes,
    // so the root node will always be -1 and the other nodes from
    // the multifile profile will obtain their original values
    int getNormalizedIndex(Integer idx) {
        int i = idx.intValue();
        if (!normalize) {
            return i;
        }

        return i == rootIndex ? -1 : i % FileParserContext.MAX_INDEX;
    }
    
    void printNode(PrintStream out, CallGraphNode cgNode) {
        out.print(getNormalizedIndex(cgNode.index));
        out.print('\t');
        out.print(getNormalizedIndex(cgNode.parentIdx));
        out.print('\t');
        out.print(cgNode.depth);
        out.print('\t');
        out.print(cgNode.name);
        out.print('\t');
        out.print(cgNode.count);
        out.print('\t');
        out.print(cgNode.onlyCycles);
        out.print('\t');
        out.print(cgNode.onlyMsec);
        out.print('\t');
        out.print(cgNode.onlyPerc);
        out.print('\t');
        out.print(cgNode.kidsCycles);
        out.print('\t');
        out.print(cgNode.kidsMsec);
        out.print('\t');
        out.print(cgNode.kidsPerc);
        out.print('\t');
        out.print(cgNode.startTime);
        out.print('\t');
        out.print(cgNode.endTime);
        out.println("\t0\t0"); // dummies
        
        Vector v = cgNode.children;
        if (v != null) {
            for (int i = 0; i < v.size(); i++) {
                printNode(out, (CallGraphNode)v.elementAt(i));
            }
        }
    }

    void exportTo(File file) {
        PrintStream out;
        try {
            out = new PrintStream(new FileOutputStream(file));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return;
        }
        out.println("# Generated by ProfView");
        printNode(out, root);
        out.close();
    }
}

public class ProfView implements TreeSelectionListener, ListSelectionListener,
                                 DocumentListener, ActionListener,
                                 TimeLineListener
{
    public static void main(String argv[]) {
        if (argv.length == 0) {
            System.out.println("No graph file specified!");
            System.out.println("Usage: java -cp ProfView.jar ProfView graphfile1 ... graphfileN");
            System.exit(1);
        }

        ProfView viewer = new ProfView();
        viewer.start(argv);
    }

    final static Color DELTA_COLOR = new Color(0, 0, 160);
    final static Color DEFAULT_COLOR = Color.BLACK;

    JFrame frame;
    ProfileTree tree;
    JTable table;
    MainTableModel tableModel;
    GotoFrame gotoFrame;
    CalleeFrame calleeFrame;
    Profile profile;
    JLabel tableLabel;
    JCheckBox geomMean;
    MyTreeNode topTreeNode;
    MyTreeNode currentTreeNode;
    CallGraphNode deltaTarget;
    JFrame deltaPrompt;
    TimeLine timeline;

    javax.swing.Timer timer;
    JTextField filterText;
    Filter filter;

    void start(String[] files) {
        try {
            // UIManager.setLookAndFeel(
            //     "com.sun.java.swing.plaf.windows.WindowsLookAndFeel");

            this.profile = new Profile(files);
            
            String caption = files[0];
            for (int i = 1; i < files.length; i++) {
                caption += " + " + files[i];
            }
            frame = new JFrame("ProfView " + caption);

            JPanel treePanel = createTreePanel(profile);
            JPanel tablePanel = createTablePanel(profile);

            JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
            splitPane.setTopComponent(treePanel);
            splitPane.setBottomComponent(tablePanel);

            frame.getContentPane().add(splitPane, BorderLayout.CENTER);

            frame.pack();
            frame.setLocation(50, 50);
            frame.setSize(900, 600);
            frame.setVisible(true);

            frame.addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    System.exit(0);
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    JScrollPane createTableView(Profile profile) {
        tableModel = new MainTableModel(this);
        table = new ProfileTable(tableModel, this);
        tableModel.setCurrentDataSet(profile.root);
        tableModel.setOwner(table);

        JScrollPane tableView = new JScrollPane(table);
        tableView.setMinimumSize(new Dimension(180, 400));
        tableView.setPreferredSize(new Dimension(680, 400));

        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);

        ListSelectionModel rowSM = table.getSelectionModel();
        rowSM.addListSelectionListener(this);

        return tableView;
    }

    JPanel createTablePanel(Profile profile) {
        // The table scroll pane
        JScrollPane tableView = createTableView(profile);
        
        // The button panel
        JPanel btnPanel = new JPanel();
        GridBagLayout gridbag = new GridBagLayout();
        GridBagConstraints c = new GridBagConstraints();

        btnPanel.setLayout(gridbag);
        btnPanel.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));

        filterText = new JTextField(20);
        filterText.setActionCommand("filter_main_table");
        filterText.addActionListener(this);
        filterText.getDocument().addDocumentListener(this);

        JLabel filterLabel = new JLabel("Filter:");
        filterLabel.setLabelFor(filterText);

        // used for filter updating after filterText change
        timer = new javax.swing.Timer(500, this);

        JButton callee = new JButton("Callees");
        callee.setActionCommand("callee_main_table");
        callee.addActionListener(this);

        JButton caller = new JButton("Callers");
        caller.setActionCommand("caller_main_table");
        caller.addActionListener(this);

        timeline = new TimeLine(profile.period);
        timeline.setTimeLineListener(this);

        c.insets = new Insets(2, 2, 2, 2);
        c.fill = GridBagConstraints.HORIZONTAL;
        c.gridwidth = GridBagConstraints.REMAINDER;
        c.weightx = 1.0;
        gridbag.setConstraints(timeline, c);
        btnPanel.add(timeline);
        
        c.gridwidth = 1;
        c.weightx = 0.0;
        gridbag.setConstraints(filterLabel, c);
        btnPanel.add(filterLabel);

        c.weightx = 1.0;
        gridbag.setConstraints(filterText, c);
        btnPanel.add(filterText);

        c.weightx = 0.0;
        gridbag.setConstraints(callee, c);
        btnPanel.add(callee);

        gridbag.setConstraints(caller, c);
        btnPanel.add(caller);

        // Put them together
        JPanel tablePanel = new JPanel();
        tablePanel.setLayout(new BorderLayout());
        tableLabel = new JLabel("All calls under <root>");
        tablePanel.add(tableLabel, BorderLayout.NORTH);
        tablePanel.add(btnPanel, BorderLayout.SOUTH);
        tablePanel.add(tableView, BorderLayout.CENTER);

        return tablePanel;
    }

    JPanel createTreePanel(Profile profile) {
        currentTreeNode = topTreeNode = createNodes(profile.root);

        tree = new ProfileTree(topTreeNode);
        tree.addTreeSelectionListener(this);
        tree.addTreeListener(this);
        tree.getSelectionModel().setSelectionMode
            (TreeSelectionModel.SINGLE_TREE_SELECTION);

        JScrollPane scrollPane = new JScrollPane(tree);
        scrollPane.setMinimumSize(new Dimension(180, 400));
        scrollPane.setPreferredSize(new Dimension(600, 500));

        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());
        panel.add(new JLabel("Call Graph"), BorderLayout.NORTH);
        panel.add(scrollPane, BorderLayout.CENTER);
        
        if (profile.isMultifile()) {
            // "Geom mean" checkbox is efficient only for multifile profiles
            geomMean = new JCheckBox("Merge using geom mean");
            geomMean.setActionCommand("geom_mean_click");
            geomMean.addActionListener(this);
            JPanel mergePanel = new JPanel();
            mergePanel.setBorder(BorderFactory.createEmptyBorder(2,2,2,2));
            mergePanel.add(geomMean);
            panel.add(mergePanel, BorderLayout.SOUTH);
        }

        return panel;
    }

    MyTreeNode createNodes(CallGraphNode cgNode) {
        MyTreeNode trNode = new MyTreeNode(cgNode);
        if (cgNode.children != null) {
            Vector v = cgNode.children;
            for (int i=0; i<v.size(); i++) {
                CallGraphNode child = (CallGraphNode)v.elementAt(i);
                trNode.add(createNodes(child));
            }
        }

        return trNode;
    }

    /**
     * Handles Tree selection changes.
     */
    public void valueChanged(TreeSelectionEvent e) {
        MyTreeNode n =  (MyTreeNode) tree.getLastSelectedPathComponent();
        if (n != currentTreeNode) {
            currentTreeNode = n;
            if (deltaTarget != null) {
                currentTreeNode.cgNode.setDelta(deltaTarget);
                deltaTarget = null;
                deltaPrompt.setVisible(false);
            }
            refreshTable();
        }
    }
        
    void refreshTable() {
        if (currentTreeNode == null) {
            return;
        }

        tableLabel.setText("All calls under " + currentTreeNode.cgNode.name);

        tableModel.setCurrentDataSet(currentTreeNode.cgNode);
        tableModel.updateTable(false);

        ListSelectionModel rowSM = table.getSelectionModel();
        rowSM.clearSelection();
    }

    /**
     * Handles Table selection changes. Implements 
     * ListSelectionListener.valueChanged
     */
    public void valueChanged(ListSelectionEvent e) {
        if (e.getValueIsAdjusting()) {
            return;
        }
        
        ListSelectionModel lsm = (ListSelectionModel)e.getSource();
        if (!lsm.isSelectionEmpty()) {
            int selectedRow = lsm.getMinSelectionIndex();
            tableRowSelected(selectedRow);
        }
    }

    void tableRowSelected(int row) {
        CallRecord rec = tableModel.getRecordByRow(row);
        timeline.select(rec.startTime, rec.endTime);
        if (gotoFrame != null && gotoFrame.isVisible()) {
            gotoFrame.display(rec.name);
        }
        if (calleeFrame != null && calleeFrame.isVisible()) {
            calleeFrame.display(rec.name);
        }
    }

    /**
     * Handles filter changes. Implements DocumentListener
     */
    public void insertUpdate(javax.swing.event.DocumentEvent e) {
        timer.stop();
        timer.start();
    }

    public void removeUpdate(javax.swing.event.DocumentEvent e) {
        timer.stop();
        timer.start();
    }

    public void changedUpdate(javax.swing.event.DocumentEvent e) {}

    /**
     * Handle TimeLine actions
     */
    public void markerChanged() {
        timer.stop();
        timer.start();
    }

    /**
     * Handle general actions
     */
    public void actionPerformed(java.awt.event.ActionEvent e) {
        String command = e.getActionCommand(); // null means Timer event
        boolean alt = (e.getModifiers() & e.SHIFT_MASK) != 0;
        if (command == null || command.equals("filter_main_table")) {
            timer.stop();
            Filter f = new Filter(filterText.getText(),
                                  timeline.getMarkedTime());
            if (!f.equals(filter)) {
                filter = f.isEmpty() ? null : f;
                refreshTable();
            }
        }
        else if (command.equals("caller_main_table") ||
                 command.equals("table_dblclick") && !alt) {
            if (gotoFrame == null) {
                gotoFrame = new GotoFrame(this, profile);
            }
            if (!gotoFrame.isVisible()) {
                gotoFrame.setVisible(true);
                ListSelectionModel lsm = table.getSelectionModel();
                if (!lsm.isSelectionEmpty()) {
                    tableRowSelected(lsm.getMinSelectionIndex());
                }
            }
            ListSelectionModel lsm = gotoFrame.siteTable.getSelectionModel();
            lsm.clearSelection();
            lsm.addSelectionInterval(0, 0);
            gotoFrame.toFront();
        }
        else if (command.equals("callee_main_table") ||
                 command.equals("table_dblclick") && alt) {
            if (calleeFrame == null) {
                calleeFrame = new CalleeFrame(this, profile);
            }
            if (!calleeFrame.isVisible()) {
                calleeFrame.setVisible(true);
                ListSelectionModel lsm = table.getSelectionModel();
                if (!lsm.isSelectionEmpty()) {
                    tableRowSelected(lsm.getMinSelectionIndex());
                }
            }
            calleeFrame.toFront();
        }
        else if (command.equals("geom_mean_click")) {
            AveragedCallRecord.averagingMethod = 
                ((JCheckBox)e.getSource()).isSelected() ?
                    AveragedCallRecord.GEOMETRIC_MEAN :
                    AveragedCallRecord.ARITHMETIC_MEAN;
            profile.root.computePercentage();
            refreshTable();
        }
        else if (command.equals("tree_export")) {
            File file = chooseFileName(
                "Export " + currentTreeNode.cgNode.name + " subtree to");
            if (file != null) {
                boolean needToNormalize = currentTreeNode != topTreeNode;
                (new ProfileWriter(currentTreeNode.cgNode, needToNormalize)).
                    exportTo(file);
            }
        }
        else if (command.equals("tree_delta")) {
            deltaTarget = currentTreeNode.cgNode;
            showDeltaPrompt();
        }
        else if (command.equals("tree_cancel")) {
            deltaTarget = null;
            deltaPrompt.setVisible(false);
        }
        else if (command.equals("tree_restore")) {
            currentTreeNode.cgNode.clearDelta();
            refreshTable();
        }
    }

    void showDeltaPrompt() {
        if (deltaPrompt == null) {
            deltaPrompt = new JFrame("Calculate profiles Delta");
            JButton cancel = new JButton("Cancel");
            cancel.setActionCommand("tree_cancel");
            cancel.addActionListener(this);

            Container pane = deltaPrompt.getContentPane();
            pane.setLayout(new GridLayout(2, 1));
            pane.add(new JLabel(" Please, click the tree node" +
                                " that will be compared to the selection "));
            pane.add(cancel);
            deltaPrompt.pack();
            deltaPrompt.setLocation(300, 100);
        }
        deltaPrompt.setVisible(true);
    }

    TreePath getTreeTath(CallGraphNode cgNode) {
        if (cgNode.parent == null) {
            return new TreePath(topTreeNode);
        } else {
            TreePath treePath = getTreeTath(cgNode.parent);
            MyTreeNode parentTreeNode = 
                (MyTreeNode)treePath.getLastPathComponent();

            for (Enumeration e = parentTreeNode.children();
                 e.hasMoreElements();) {
                MyTreeNode treeNode = (MyTreeNode)e.nextElement();

                if (treeNode.cgNode == cgNode) {
                    return treePath.pathByAddingChild(treeNode);
                }
            }
            throw new RuntimeException("Shouldn't come to here!");
        }
    }

    void gotoGraph(CallGraphNode cgNode) {
        TreePath treePath = getTreeTath(cgNode);
        tree.scrollPathToVisible(treePath);
        tree.setSelectionPath(treePath);
    }

    File chooseFileName(String caption) {
        JFileChooser filebox = new JFileChooser();
        filebox.setDialogTitle(caption);
        if (filebox.showSaveDialog(frame) == filebox.APPROVE_OPTION) {
            return filebox.getSelectedFile();
        }
        return null;
    }
}
