/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ddmuilib;

import com.android.ddmlib.AndroidDebugBridge.IClientChangeListener;
import com.android.ddmlib.Client;
import com.android.ddmlib.ClientData;
import com.android.ddmlib.HeapSegment.HeapSegmentElement;
import com.android.ddmlib.Log;

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.custom.StackLayout;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Group;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.labels.CategoryToolTipGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.Plot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.CategoryItemRenderer;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.experimental.chart.swt.ChartComposite;
import org.jfree.experimental.swt.SWTUtils;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;


/**
 * Base class for our information panels.
 */
public final class HeapPanel extends BaseHeapPanel {
    private static final String PREFS_STATS_COL_TYPE = "heapPanel.col0"; //$NON-NLS-1$
    private static final String PREFS_STATS_COL_COUNT = "heapPanel.col1"; //$NON-NLS-1$
    private static final String PREFS_STATS_COL_SIZE = "heapPanel.col2"; //$NON-NLS-1$
    private static final String PREFS_STATS_COL_SMALLEST = "heapPanel.col3"; //$NON-NLS-1$
    private static final String PREFS_STATS_COL_LARGEST = "heapPanel.col4"; //$NON-NLS-1$
    private static final String PREFS_STATS_COL_MEDIAN = "heapPanel.col5"; //$NON-NLS-1$
    private static final String PREFS_STATS_COL_AVERAGE = "heapPanel.col6"; //$NON-NLS-1$

    /* args to setUpdateStatus() */
    private static final int NOT_SELECTED   = 0;
    private static final int NOT_ENABLED    = 1;
    private static final int ENABLED        = 2;

    /** color palette and map legend. NATIVE is the last enum is a 0 based enum list, so we need
     * Native+1 at least. We also need 2 more entries for free area and expansion area.  */
    private static final int NUM_PALETTE_ENTRIES = HeapSegmentElement.KIND_NATIVE+2 +1;
    private static final String[] mMapLegend = new String[NUM_PALETTE_ENTRIES];
    private static final PaletteData mMapPalette = createPalette();

    private static final boolean DISPLAY_HEAP_BITMAP = false;
    private static final boolean DISPLAY_HILBERT_BITMAP = false;

    private static final int PLACEHOLDER_HILBERT_SIZE = 200;
    private static final int PLACEHOLDER_LINEAR_V_SIZE = 100;
    private static final int PLACEHOLDER_LINEAR_H_SIZE = 300;

    private static final int[] ZOOMS = {100, 50, 25};

    private static final NumberFormat sByteFormatter = NumberFormat.getInstance();
    private static final NumberFormat sLargeByteFormatter = NumberFormat.getInstance();
    private static final NumberFormat sCountFormatter = NumberFormat.getInstance();

    static {
        sByteFormatter.setMinimumFractionDigits(0);
        sByteFormatter.setMaximumFractionDigits(1);
        sLargeByteFormatter.setMinimumFractionDigits(3);
        sLargeByteFormatter.setMaximumFractionDigits(3);

        sCountFormatter.setGroupingUsed(true);
    }

    private Display mDisplay;

    private Composite mTop; // real top
    private Label mUpdateStatus;
    private Table mHeapSummary;
    private Combo mDisplayMode;

    //private ScrolledComposite mScrolledComposite;

    private Composite mDisplayBase; // base of the displays.
    private StackLayout mDisplayStack;

    private Composite mStatisticsBase;
    private Table mStatisticsTable;
    private JFreeChart mChart;
    private ChartComposite mChartComposite;
    private Button mGcButton;
    private DefaultCategoryDataset mAllocCountDataSet;

    private Composite mLinearBase;
    private Label mLinearHeapImage;

    private Composite mHilbertBase;
    private Label mHilbertHeapImage;
    private Group mLegend;
    private Combo mZoom;

    /** Image used for the hilbert display. Since we recreate a new image every time, we
     * keep this one around to dispose it. */
    private Image mHilbertImage;
    private Image mLinearImage;
    private Composite[] mLayout;

    /*
     * Create color palette for map.  Set up titles for legend.
     */
    private static PaletteData createPalette() {
        RGB colors[] = new RGB[NUM_PALETTE_ENTRIES];
        colors[0]
                = new RGB(192, 192, 192); // non-heap pixels are gray
        mMapLegend[0]
                = "(heap expansion area)";

        colors[1]
                = new RGB(0, 0, 0);       // free chunks are black
        mMapLegend[1]
                = "free";

        colors[HeapSegmentElement.KIND_OBJECT + 2]
                = new RGB(0, 0, 255);     // objects are blue
        mMapLegend[HeapSegmentElement.KIND_OBJECT + 2]
                = "data object";

        colors[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
                = new RGB(0, 255, 0);     // class objects are green
        mMapLegend[HeapSegmentElement.KIND_CLASS_OBJECT + 2]
                = "class object";

        colors[HeapSegmentElement.KIND_ARRAY_1 + 2]
                = new RGB(255, 0, 0);     // byte/bool arrays are red
        mMapLegend[HeapSegmentElement.KIND_ARRAY_1 + 2]
                = "1-byte array (byte[], boolean[])";

        colors[HeapSegmentElement.KIND_ARRAY_2 + 2]
                = new RGB(255, 128, 0);   // short/char arrays are orange
        mMapLegend[HeapSegmentElement.KIND_ARRAY_2 + 2]
                = "2-byte array (short[], char[])";

        colors[HeapSegmentElement.KIND_ARRAY_4 + 2]
                = new RGB(255, 255, 0);   // obj/int/float arrays are yellow
        mMapLegend[HeapSegmentElement.KIND_ARRAY_4 + 2]
                = "4-byte array (object[], int[], float[])";

        colors[HeapSegmentElement.KIND_ARRAY_8 + 2]
                = new RGB(255, 128, 128); // long/double arrays are pink
        mMapLegend[HeapSegmentElement.KIND_ARRAY_8 + 2]
                = "8-byte array (long[], double[])";

        colors[HeapSegmentElement.KIND_UNKNOWN + 2]
                = new RGB(255, 0, 255);   // unknown objects are cyan
        mMapLegend[HeapSegmentElement.KIND_UNKNOWN + 2]
                = "unknown object";

        colors[HeapSegmentElement.KIND_NATIVE + 2]
                = new RGB(64, 64, 64);    // native objects are dark gray
        mMapLegend[HeapSegmentElement.KIND_NATIVE + 2]
                = "non-Java object";

        return new PaletteData(colors);
    }

    /**
     * Sent when an existing client information changed.
     * <p/>
     * This is sent from a non UI thread.
     * @param client the updated client.
     * @param changeMask the bit mask describing the changed properties. It can contain
     * any of the following values: {@link Client#CHANGE_INFO}, {@link Client#CHANGE_NAME}
     * {@link Client#CHANGE_DEBUGGER_STATUS}, {@link Client#CHANGE_THREAD_MODE},
     * {@link Client#CHANGE_THREAD_DATA}, {@link Client#CHANGE_HEAP_MODE},
     * {@link Client#CHANGE_HEAP_DATA}, {@link Client#CHANGE_NATIVE_HEAP_DATA}
     *
     * @see IClientChangeListener#clientChanged(Client, int)
     */
    @Override
    public void clientChanged(final Client client, int changeMask) {
        if (client == getCurrentClient()) {
            if ((changeMask & Client.CHANGE_HEAP_MODE) == Client.CHANGE_HEAP_MODE ||
                    (changeMask & Client.CHANGE_HEAP_DATA) == Client.CHANGE_HEAP_DATA) {
                try {
                    mTop.getDisplay().asyncExec(new Runnable() {
                        @Override
                        public void run() {
                            clientSelected();
                        }
                    });
                } catch (SWTException e) {
                    // display is disposed (app is quitting most likely), we do nothing.
                }
            }
        }
    }

    /**
     * Sent when a new device is selected. The new device can be accessed
     * with {@link #getCurrentDevice()}
     */
    @Override
    public void deviceSelected() {
        // pass
    }

    /**
     * Sent when a new client is selected. The new client can be accessed
     * with {@link #getCurrentClient()}.
     */
    @Override
    public void clientSelected() {
        if (mTop.isDisposed())
            return;

        Client client = getCurrentClient();

        Log.d("ddms", "HeapPanel: changed " + client);

        if (client != null) {
            ClientData cd = client.getClientData();

            if (client.isHeapUpdateEnabled()) {
                mGcButton.setEnabled(true);
                mDisplayMode.setEnabled(true);
                setUpdateStatus(ENABLED);
            } else {
                setUpdateStatus(NOT_ENABLED);
                mGcButton.setEnabled(false);
                mDisplayMode.setEnabled(false);
            }

            fillSummaryTable(cd);

            int mode = mDisplayMode.getSelectionIndex();
            if (mode == 0) {
                fillDetailedTable(client, false /* forceRedraw */);
            } else {
                if (DISPLAY_HEAP_BITMAP) {
                    renderHeapData(cd, mode - 1, false /* forceRedraw */);
                }
            }
        } else {
            mGcButton.setEnabled(false);
            mDisplayMode.setEnabled(false);
            fillSummaryTable(null);
            fillDetailedTable(null, true);
            setUpdateStatus(NOT_SELECTED);
        }

        // sizes of things change frequently, so redo layout
        //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
        //        SWT.DEFAULT));
        mDisplayBase.layout();
        //mScrolledComposite.redraw();
    }

    /**
     * Create our control(s).
     */
    @Override
    protected Control createControl(Composite parent) {
        mDisplay = parent.getDisplay();

        GridLayout gl;

        mTop = new Composite(parent, SWT.NONE);
        mTop.setLayout(new GridLayout(1, false));
        mTop.setLayoutData(new GridData(GridData.FILL_BOTH));

        mUpdateStatus = new Label(mTop, SWT.NONE);
        setUpdateStatus(NOT_SELECTED);

        Composite summarySection = new Composite(mTop, SWT.NONE);
        summarySection.setLayout(gl = new GridLayout(2, false));
        gl.marginHeight = gl.marginWidth = 0;

        mHeapSummary = createSummaryTable(summarySection);
        mGcButton = new Button(summarySection, SWT.PUSH);
        mGcButton.setText("Cause GC");
        mGcButton.setEnabled(false);
        mGcButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                Client client = getCurrentClient();
                if (client != null) {
                    client.executeGarbageCollector();
                }
            }
        });

        Composite comboSection = new Composite(mTop, SWT.NONE);
        gl = new GridLayout(2, false);
        gl.marginHeight = gl.marginWidth = 0;
        comboSection.setLayout(gl);

        Label displayLabel = new Label(comboSection, SWT.NONE);
        displayLabel.setText("Display: ");

        mDisplayMode = new Combo(comboSection, SWT.READ_ONLY);
        mDisplayMode.setEnabled(false);
        mDisplayMode.add("Stats");
        if (DISPLAY_HEAP_BITMAP) {
            mDisplayMode.add("Linear");
            if (DISPLAY_HILBERT_BITMAP) {
                mDisplayMode.add("Hilbert");
            }
        }

        // the base of the displays.
        mDisplayBase = new Composite(mTop, SWT.NONE);
        mDisplayBase.setLayoutData(new GridData(GridData.FILL_BOTH));
        mDisplayStack = new StackLayout();
        mDisplayBase.setLayout(mDisplayStack);

        // create the statistics display
        mStatisticsBase = new Composite(mDisplayBase, SWT.NONE);
        //mStatisticsBase.setLayoutData(new GridData(GridData.FILL_BOTH));
        mStatisticsBase.setLayout(gl = new GridLayout(1, false));
        gl.marginHeight = gl.marginWidth = 0;
        mDisplayStack.topControl = mStatisticsBase;

        mStatisticsTable = createDetailedTable(mStatisticsBase);
        mStatisticsTable.setLayoutData(new GridData(GridData.FILL_BOTH));

        createChart();

        //create the linear composite
        mLinearBase = new Composite(mDisplayBase, SWT.NONE);
        //mLinearBase.setLayoutData(new GridData());
        gl = new GridLayout(1, false);
        gl.marginHeight = gl.marginWidth = 0;
        mLinearBase.setLayout(gl);

        {
            mLinearHeapImage = new Label(mLinearBase, SWT.NONE);
            mLinearHeapImage.setLayoutData(new GridData());
            mLinearHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay,
                    PLACEHOLDER_LINEAR_H_SIZE, PLACEHOLDER_LINEAR_V_SIZE,
                    mDisplay.getSystemColor(SWT.COLOR_BLUE)));

            // create a composite to contain the bottom part (legend)
            Composite bottomSection = new Composite(mLinearBase, SWT.NONE);
            gl = new GridLayout(1, false);
            gl.marginHeight = gl.marginWidth = 0;
            bottomSection.setLayout(gl);

            createLegend(bottomSection);
        }

/*
        mScrolledComposite = new ScrolledComposite(mTop, SWT.H_SCROLL | SWT.V_SCROLL);
        mScrolledComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
        mScrolledComposite.setExpandHorizontal(true);
        mScrolledComposite.setExpandVertical(true);
        mScrolledComposite.setContent(mDisplayBase);
*/


        // create the hilbert display.
        mHilbertBase = new Composite(mDisplayBase, SWT.NONE);
        //mHilbertBase.setLayoutData(new GridData());
        gl = new GridLayout(2, false);
        gl.marginHeight = gl.marginWidth = 0;
        mHilbertBase.setLayout(gl);

        if (DISPLAY_HILBERT_BITMAP) {
            mHilbertHeapImage = new Label(mHilbertBase, SWT.NONE);
            mHilbertHeapImage.setLayoutData(new GridData());
            mHilbertHeapImage.setImage(ImageLoader.createPlaceHolderArt(mDisplay,
                    PLACEHOLDER_HILBERT_SIZE, PLACEHOLDER_HILBERT_SIZE,
                    mDisplay.getSystemColor(SWT.COLOR_BLUE)));

            // create a composite to contain the right part (legend + zoom)
            Composite rightSection = new Composite(mHilbertBase, SWT.NONE);
            gl = new GridLayout(1, false);
            gl.marginHeight = gl.marginWidth = 0;
            rightSection.setLayout(gl);

            Composite zoomComposite = new Composite(rightSection, SWT.NONE);
            gl = new GridLayout(2, false);
            zoomComposite.setLayout(gl);

            Label l = new Label(zoomComposite, SWT.NONE);
            l.setText("Zoom:");
            mZoom = new Combo(zoomComposite, SWT.READ_ONLY);
            for (int z : ZOOMS) {
                mZoom.add(String.format("%1$d%%", z)); //$NON-NLS-1$
            }

            mZoom.select(0);
            mZoom.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    setLegendText(mZoom.getSelectionIndex());
                    Client client = getCurrentClient();
                    if (client != null) {
                        renderHeapData(client.getClientData(), 1, true);
                        mTop.pack();
                    }
                }
            });

            createLegend(rightSection);
        }
        mHilbertBase.pack();

        mLayout = new Composite[] { mStatisticsBase, mLinearBase, mHilbertBase };
        mDisplayMode.select(0);
        mDisplayMode.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                int index = mDisplayMode.getSelectionIndex();
                Client client = getCurrentClient();

                if (client != null) {
                    if (index == 0) {
                        fillDetailedTable(client, true /* forceRedraw */);
                    } else {
                        renderHeapData(client.getClientData(), index-1, true /* forceRedraw */);
                    }
                }

                mDisplayStack.topControl = mLayout[index];
                //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
                //        SWT.DEFAULT));
                mDisplayBase.layout();
                //mScrolledComposite.redraw();
            }
        });

        //mScrolledComposite.setMinSize(mDisplayStack.topControl.computeSize(SWT.DEFAULT,
        //        SWT.DEFAULT));
        mDisplayBase.layout();
        //mScrolledComposite.redraw();

        return mTop;
    }

    /**
     * Sets the focus to the proper control inside the panel.
     */
    @Override
    public void setFocus() {
        mHeapSummary.setFocus();
    }


    private Table createSummaryTable(Composite base) {
        Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
        tab.setHeaderVisible(true);
        tab.setLinesVisible(true);

        TableColumn col;

        col = new TableColumn(tab, SWT.RIGHT);
        col.setText("ID");
        col.pack();

        col = new TableColumn(tab, SWT.RIGHT);
        col.setText("000.000WW"); //$NON-NLS-1$
        col.pack();
        col.setText("Heap Size");

        col = new TableColumn(tab, SWT.RIGHT);
        col.setText("000.000WW"); //$NON-NLS-1$
        col.pack();
        col.setText("Allocated");

        col = new TableColumn(tab, SWT.RIGHT);
        col.setText("000.000WW"); //$NON-NLS-1$
        col.pack();
        col.setText("Free");

        col = new TableColumn(tab, SWT.RIGHT);
        col.setText("000.00%"); //$NON-NLS-1$
        col.pack();
        col.setText("% Used");

        col = new TableColumn(tab, SWT.RIGHT);
        col.setText("000,000,000"); //$NON-NLS-1$
        col.pack();
        col.setText("# Objects");

        // make sure there is always one empty item so that one table row is always displayed.
        TableItem item = new TableItem(tab, SWT.NONE);
        item.setText("");

        return tab;
    }

    private Table createDetailedTable(Composite base) {
        IPreferenceStore store = DdmUiPreferences.getStore();

        Table tab = new Table(base, SWT.SINGLE | SWT.FULL_SELECTION);
        tab.setHeaderVisible(true);
        tab.setLinesVisible(true);

        TableHelper.createTableColumn(tab, "Type", SWT.LEFT,
                "4-byte array (object[], int[], float[])", //$NON-NLS-1$
                PREFS_STATS_COL_TYPE, store);

        TableHelper.createTableColumn(tab, "Count", SWT.RIGHT,
                "00,000", //$NON-NLS-1$
                PREFS_STATS_COL_COUNT, store);

        TableHelper.createTableColumn(tab, "Total Size", SWT.RIGHT,
                "000.000 WW", //$NON-NLS-1$
                PREFS_STATS_COL_SIZE, store);

        TableHelper.createTableColumn(tab, "Smallest", SWT.RIGHT,
                "000.000 WW", //$NON-NLS-1$
                PREFS_STATS_COL_SMALLEST, store);

        TableHelper.createTableColumn(tab, "Largest", SWT.RIGHT,
                "000.000 WW", //$NON-NLS-1$
                PREFS_STATS_COL_LARGEST, store);

        TableHelper.createTableColumn(tab, "Median", SWT.RIGHT,
                "000.000 WW", //$NON-NLS-1$
                PREFS_STATS_COL_MEDIAN, store);

        TableHelper.createTableColumn(tab, "Average", SWT.RIGHT,
                "000.000 WW", //$NON-NLS-1$
                PREFS_STATS_COL_AVERAGE, store);

        tab.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {

                Client client = getCurrentClient();
                if (client != null) {
                    int index = mStatisticsTable.getSelectionIndex();
                    TableItem item = mStatisticsTable.getItem(index);

                    if (item != null) {
                        Map<Integer, ArrayList<HeapSegmentElement>> heapMap =
                            client.getClientData().getVmHeapData().getProcessedHeapMap();

                        ArrayList<HeapSegmentElement> list = heapMap.get(item.getData());
                        if (list != null) {
                            showChart(list);
                        }
                    }
                }

            }
        });

        return tab;
    }

    /**
     * Creates the chart below the statistics table
     */
    private void createChart() {
        mAllocCountDataSet = new DefaultCategoryDataset();
        mChart = ChartFactory.createBarChart(null, "Size", "Count", mAllocCountDataSet,
                PlotOrientation.VERTICAL, false, true, false);

        // get the font to make a proper title. We need to convert the swt font,
        // into an awt font.
        Font f = mStatisticsBase.getFont();
        FontData[] fData = f.getFontData();

        // event though on Mac OS there could be more than one fontData, we'll only use
        // the first one.
        FontData firstFontData = fData[0];

        java.awt.Font awtFont = SWTUtils.toAwtFont(mStatisticsBase.getDisplay(),
                firstFontData, true /* ensureSameSize */);

        mChart.setTitle(new TextTitle("Allocation count per size", awtFont));

        Plot plot = mChart.getPlot();
        if (plot instanceof CategoryPlot) {
            // get the plot
            CategoryPlot categoryPlot = (CategoryPlot)plot;

            // set the domain axis to draw labels that are displayed even with many values.
            CategoryAxis domainAxis = categoryPlot.getDomainAxis();
            domainAxis.setCategoryLabelPositions(CategoryLabelPositions.DOWN_90);

            CategoryItemRenderer renderer = categoryPlot.getRenderer();
            renderer.setBaseToolTipGenerator(new CategoryToolTipGenerator() {
                @Override
                public String generateToolTip(CategoryDataset dataset, int row, int column) {
                    // get the key for the size of the allocation
                    ByteLong columnKey = (ByteLong)dataset.getColumnKey(column);
                    String rowKey = (String)dataset.getRowKey(row);
                    Number value = dataset.getValue(rowKey, columnKey);

                    return String.format("%1$d %2$s of %3$d bytes", value.intValue(), rowKey,
                            columnKey.getValue());
                }
            });
        }
        mChartComposite = new ChartComposite(mStatisticsBase, SWT.BORDER, mChart,
                ChartComposite.DEFAULT_WIDTH,
                ChartComposite.DEFAULT_HEIGHT,
                ChartComposite.DEFAULT_MINIMUM_DRAW_WIDTH,
                ChartComposite.DEFAULT_MINIMUM_DRAW_HEIGHT,
                3000, // max draw width. We don't want it to zoom, so we put a big number
                3000, // max draw height. We don't want it to zoom, so we put a big number
                true,  // off-screen buffer
                true,  // properties
                true,  // save
                true,  // print
                false,  // zoom
                true);   // tooltips

        mChartComposite.setLayoutData(new GridData(GridData.FILL_BOTH));
    }

    private static String prettyByteCount(long bytes) {
        double fracBytes = bytes;
        String units = " B";
        if (fracBytes < 1024) {
            return sByteFormatter.format(fracBytes) + units;
        } else {
            fracBytes /= 1024;
            units = " KB";
        }
        if (fracBytes >= 1024) {
            fracBytes /= 1024;
            units = " MB";
        }
        if (fracBytes >= 1024) {
            fracBytes /= 1024;
            units = " GB";
        }

        return sLargeByteFormatter.format(fracBytes) + units;
    }

    private static String approximateByteCount(long bytes) {
        double fracBytes = bytes;
        String units = "";
        if (fracBytes >= 1024) {
            fracBytes /= 1024;
            units = "K";
        }
        if (fracBytes >= 1024) {
            fracBytes /= 1024;
            units = "M";
        }
        if (fracBytes >= 1024) {
            fracBytes /= 1024;
            units = "G";
        }

        return sByteFormatter.format(fracBytes) + units;
    }

    private static String addCommasToNumber(long num) {
        return sCountFormatter.format(num);
    }

    private static String fractionalPercent(long num, long denom) {
        double val = (double)num / (double)denom;
        val *= 100;

        NumberFormat nf = NumberFormat.getInstance();
        nf.setMinimumFractionDigits(2);
        nf.setMaximumFractionDigits(2);
        return nf.format(val) + "%";
    }

    private void fillSummaryTable(ClientData cd) {
        if (mHeapSummary.isDisposed()) {
            return;
        }

        mHeapSummary.setRedraw(false);
        mHeapSummary.removeAll();

        int numRows = 0;
        if (cd != null) {
            synchronized (cd) {
                Iterator<Integer> iter = cd.getVmHeapIds();

                while (iter.hasNext()) {
                    numRows++;
                    Integer id = iter.next();
                    Map<String, Long> heapInfo = cd.getVmHeapInfo(id);
                    if (heapInfo == null) {
                        continue;
                    }
                    long sizeInBytes = heapInfo.get(ClientData.HEAP_SIZE_BYTES);
                    long bytesAllocated = heapInfo.get(ClientData.HEAP_BYTES_ALLOCATED);
                    long objectsAllocated = heapInfo.get(ClientData.HEAP_OBJECTS_ALLOCATED);

                    TableItem item = new TableItem(mHeapSummary, SWT.NONE);
                    item.setText(0, id.toString());

                    item.setText(1, prettyByteCount(sizeInBytes));
                    item.setText(2, prettyByteCount(bytesAllocated));
                    item.setText(3, prettyByteCount(sizeInBytes - bytesAllocated));
                    item.setText(4, fractionalPercent(bytesAllocated, sizeInBytes));
                    item.setText(5, addCommasToNumber(objectsAllocated));
                }
            }
        }

        if (numRows == 0) {
            // make sure there is always one empty item so that one table row is always displayed.
            TableItem item = new TableItem(mHeapSummary, SWT.NONE);
            item.setText("");
        }

        mHeapSummary.pack();
        mHeapSummary.setRedraw(true);
    }

    private void fillDetailedTable(Client client, boolean forceRedraw) {
        // first check if the client is invalid or heap updates are not enabled.
        if (client == null || client.isHeapUpdateEnabled() == false) {
            mStatisticsTable.removeAll();
            showChart(null);
            return;
        }

        ClientData cd = client.getClientData();

        Map<Integer, ArrayList<HeapSegmentElement>> heapMap;

        // Atomically get and clear the heap data.
        synchronized (cd) {
            if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
                // no change, we return.
                return;
            }

            heapMap = cd.getVmHeapData().getProcessedHeapMap();
        }

        // we have new data, lets display it.

        // First, get the current selection, and its key.
        int index = mStatisticsTable.getSelectionIndex();
        Integer selectedKey = null;
        if (index != -1) {
            selectedKey = (Integer)mStatisticsTable.getItem(index).getData();
        }

        // disable redraws and remove all from the table.
        mStatisticsTable.setRedraw(false);
        mStatisticsTable.removeAll();

        if (heapMap != null) {
            int selectedIndex = -1;
            ArrayList<HeapSegmentElement> selectedList = null;

            // get the keys
            Set<Integer> keys = heapMap.keySet();
            int iter = 0; // use a manual iter int because Set<?> doesn't have an index
            // based accessor.
            for (Integer key : keys) {
                ArrayList<HeapSegmentElement> list = heapMap.get(key);

                // check if this is the key that is supposed to be selected
                if (key.equals(selectedKey)) {
                    selectedIndex = iter;
                    selectedList = list;
                }
                iter++;

                TableItem item = new TableItem(mStatisticsTable, SWT.NONE);
                item.setData(key);

                // get the type
                item.setText(0, mMapLegend[key]);

                // set the count, smallest, largest
                int count = list.size();
                item.setText(1, addCommasToNumber(count));

                if (count > 0) {
                    item.setText(3, prettyByteCount(list.get(0).getLength()));
                    item.setText(4, prettyByteCount(list.get(count-1).getLength()));

                    int median = count / 2;
                    HeapSegmentElement element = list.get(median);
                    long size = element.getLength();
                    item.setText(5, prettyByteCount(size));

                    long totalSize = 0;
                    for (int i = 0 ; i < count; i++) {
                        element = list.get(i);

                        size = element.getLength();
                        totalSize += size;
                    }

                    // set the average and total
                    item.setText(2, prettyByteCount(totalSize));
                    item.setText(6, prettyByteCount(totalSize / count));
                }
            }

            mStatisticsTable.setRedraw(true);

            if (selectedIndex != -1) {
                mStatisticsTable.setSelection(selectedIndex);
                showChart(selectedList);
            } else {
                showChart(null);
            }
        } else {
            mStatisticsTable.setRedraw(true);
        }
    }

    private static class ByteLong implements Comparable<ByteLong> {
        private long mValue;

        private ByteLong(long value) {
            mValue = value;
        }

        public long getValue() {
            return mValue;
        }

        @Override
        public String toString() {
            return approximateByteCount(mValue);
        }

        @Override
        public int compareTo(ByteLong other) {
            if (mValue != other.mValue) {
                return mValue < other.mValue ? -1 : 1;
            }
            return 0;
        }

    }

    /**
     * Fills the chart with the content of the list of {@link HeapSegmentElement}.
     */
    private void showChart(ArrayList<HeapSegmentElement> list) {
        mAllocCountDataSet.clear();

        if (list != null) {
            String rowKey = "Alloc Count";

            long currentSize = -1;
            int currentCount = 0;
            for (HeapSegmentElement element : list) {
                if (element.getLength() != currentSize) {
                    if (currentSize != -1) {
                        ByteLong columnKey = new ByteLong(currentSize);
                        mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
                    }

                    currentSize = element.getLength();
                    currentCount = 1;
                } else {
                    currentCount++;
                }
            }

            // add the last item
            if (currentSize != -1) {
                ByteLong columnKey = new ByteLong(currentSize);
                mAllocCountDataSet.addValue(currentCount, rowKey, columnKey);
            }
        }
    }

    /*
     * Add a color legend to the specified table.
     */
    private void createLegend(Composite parent) {
        mLegend = new Group(parent, SWT.NONE);
        mLegend.setText(getLegendText(0));

        mLegend.setLayout(new GridLayout(2, false));

        RGB[] colors = mMapPalette.colors;

        for (int i = 0; i < NUM_PALETTE_ENTRIES; i++) {
            Image tmpImage = createColorRect(parent.getDisplay(), colors[i]);

            Label l = new Label(mLegend, SWT.NONE);
            l.setImage(tmpImage);

            l = new Label(mLegend, SWT.NONE);
            l.setText(mMapLegend[i]);
        }
    }

    private String getLegendText(int level) {
        int bytes = 8 * (100 / ZOOMS[level]);

        return String.format("Key (1 pixel = %1$d bytes)", bytes);
    }

    private void setLegendText(int level) {
        mLegend.setText(getLegendText(level));

    }

    /*
     * Create a nice rectangle in the specified color.
     */
    private Image createColorRect(Display display, RGB color) {
        int width = 32;
        int height = 16;

        Image img = new Image(display, width, height);
        GC gc = new GC(img);
        gc.setBackground(new Color(display, color));
        gc.fillRectangle(0, 0, width, height);
        gc.dispose();
        return img;
    }


    /*
     * Are updates enabled?
     */
    private void setUpdateStatus(int status) {
        switch (status) {
            case NOT_SELECTED:
                mUpdateStatus.setText("Select a client to see heap updates");
                break;
            case NOT_ENABLED:
                mUpdateStatus.setText("Heap updates are " +
                                      "NOT ENABLED for this client");
                break;
            case ENABLED:
                mUpdateStatus.setText("Heap updates will happen after " +
                                      "every GC for this client");
                break;
            default:
                throw new RuntimeException();
        }

        mUpdateStatus.pack();
    }


    /**
     * Return the closest power of two greater than or equal to value.
     *
     * @param value the return value will be >= value
     * @return a power of two >= value.  If value > 2^31, 2^31 is returned.
     */
//xxx use Integer.highestOneBit() or numberOfLeadingZeros().
    private int nextPow2(int value) {
        for (int i = 31; i >= 0; --i) {
            if ((value & (1<<i)) != 0) {
                if (i < 31) {
                    return 1<<(i + 1);
                } else {
                    return 1<<31;
                }
            }
        }
        return 0;
    }

    private int zOrderData(ImageData id, byte pixData[]) {
        int maxX = 0;
        for (int i = 0; i < pixData.length; i++) {
            /* Tread the pixData index as a z-order curve index and
             * decompose into Cartesian coordinates.
             */
            int x = (i & 1) |
                    ((i >>> 2) & 1) << 1 |
                    ((i >>> 4) & 1) << 2 |
                    ((i >>> 6) & 1) << 3 |
                    ((i >>> 8) & 1) << 4 |
                    ((i >>> 10) & 1) << 5 |
                    ((i >>> 12) & 1) << 6 |
                    ((i >>> 14) & 1) << 7 |
                    ((i >>> 16) & 1) << 8 |
                    ((i >>> 18) & 1) << 9 |
                    ((i >>> 20) & 1) << 10 |
                    ((i >>> 22) & 1) << 11 |
                    ((i >>> 24) & 1) << 12 |
                    ((i >>> 26) & 1) << 13 |
                    ((i >>> 28) & 1) << 14 |
                    ((i >>> 30) & 1) << 15;
            int y = ((i >>> 1) & 1) << 0 |
                    ((i >>> 3) & 1) << 1 |
                    ((i >>> 5) & 1) << 2 |
                    ((i >>> 7) & 1) << 3 |
                    ((i >>> 9) & 1) << 4 |
                    ((i >>> 11) & 1) << 5 |
                    ((i >>> 13) & 1) << 6 |
                    ((i >>> 15) & 1) << 7 |
                    ((i >>> 17) & 1) << 8 |
                    ((i >>> 19) & 1) << 9 |
                    ((i >>> 21) & 1) << 10 |
                    ((i >>> 23) & 1) << 11 |
                    ((i >>> 25) & 1) << 12 |
                    ((i >>> 27) & 1) << 13 |
                    ((i >>> 29) & 1) << 14 |
                    ((i >>> 31) & 1) << 15;
            try {
                id.setPixel(x, y, pixData[i]);
                if (x > maxX) {
                    maxX = x;
                }
            } catch (IllegalArgumentException ex) {
                System.out.println("bad pixels: i " + i +
                        ", w " + id.width +
                        ", h " + id.height +
                        ", x " + x +
                        ", y " + y);
                throw ex;
            }
        }
        return maxX;
    }

    private final static int HILBERT_DIR_N = 0;
    private final static int HILBERT_DIR_S = 1;
    private final static int HILBERT_DIR_E = 2;
    private final static int HILBERT_DIR_W = 3;

    private void hilbertWalk(ImageData id, InputStream pixData,
                             int order, int x, int y, int dir)
                             throws IOException {
        if (x >= id.width || y >= id.height) {
            return;
        } else if (order == 0) {
            try {
                int p = pixData.read();
                if (p >= 0) {
                    // flip along x=y axis;  assume width == height
                    id.setPixel(y, x, p);

                    /* Skanky; use an otherwise-unused ImageData field
                     * to keep track of the max x,y used. Note that x and y are inverted.
                     */
                    if (y > id.x) {
                        id.x = y;
                    }
                    if (x > id.y) {
                        id.y = x;
                    }
                }
//xxx just give up; don't bother walking the rest of the image
            } catch (IllegalArgumentException ex) {
                System.out.println("bad pixels: order " + order +
                        ", dir " + dir +
                        ", w " + id.width +
                        ", h " + id.height +
                        ", x " + x +
                        ", y " + y);
                throw ex;
            }
        } else {
            order--;
            int delta = 1 << order;
            int nextX = x + delta;
            int nextY = y + delta;

            switch (dir) {
            case HILBERT_DIR_E:
                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_N);
                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_E);
                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_E);
                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_S);
                break;
            case HILBERT_DIR_N:
                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_E);
                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_N);
                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_N);
                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_W);
                break;
            case HILBERT_DIR_S:
                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_W);
                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_S);
                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_S);
                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_E);
                break;
            case HILBERT_DIR_W:
                hilbertWalk(id, pixData, order, nextX, nextY, HILBERT_DIR_S);
                hilbertWalk(id, pixData, order, nextX,     y, HILBERT_DIR_W);
                hilbertWalk(id, pixData, order,     x,     y, HILBERT_DIR_W);
                hilbertWalk(id, pixData, order,     x, nextY, HILBERT_DIR_N);
                break;
            default:
                throw new RuntimeException("Unexpected Hilbert direction " +
                                           dir);
            }
        }
    }

    private Point hilbertOrderData(ImageData id, byte pixData[]) {

        int order = 0;
        for (int n = 1; n < id.width; n *= 2) {
            order++;
        }
        /* Skanky; use an otherwise-unused ImageData field
         * to keep track of maxX.
         */
        Point p = new Point(0,0);
        int oldIdX = id.x;
        int oldIdY = id.y;
        id.x = id.y = 0;
        try {
            hilbertWalk(id, new ByteArrayInputStream(pixData),
                        order, 0, 0, HILBERT_DIR_E);
            p.x = id.x;
            p.y = id.y;
        } catch (IOException ex) {
            System.err.println("Exception during hilbertWalk()");
            p.x = id.height;
            p.y = id.width;
        }
        id.x = oldIdX;
        id.y = oldIdY;
        return p;
    }

    private ImageData createHilbertHeapImage(byte pixData[]) {
        int w, h;

        // Pick an image size that the largest of heaps will fit into.
        w = (int)Math.sqrt(((16 * 1024 * 1024)/8));

        // Space-filling curves require a power-of-2 width.
        w = nextPow2(w);
        h = w;

        // Create the heap image.
        ImageData id = new ImageData(w, h, 8, mMapPalette);

        // Copy the data into the image
        //int maxX = zOrderData(id, pixData);
        Point maxP = hilbertOrderData(id, pixData);

        // update the max size to make it a round number once the zoom is applied
        int factor = 100 / ZOOMS[mZoom.getSelectionIndex()];
        if (factor != 1) {
            int tmp = maxP.x % factor;
            if (tmp != 0) {
                maxP.x += factor - tmp;
            }

            tmp = maxP.y % factor;
            if (tmp != 0) {
                maxP.y += factor - tmp;
            }
        }

        if (maxP.y < id.height) {
            // Crop the image down to the interesting part.
            id = new ImageData(id.width, maxP.y, id.depth, id.palette,
                               id.scanlinePad, id.data);
        }

        if (maxP.x < id.width) {
            // crop the image again. A bit trickier this time.
           ImageData croppedId = new ImageData(maxP.x, id.height, id.depth, id.palette);

           int[] buffer = new int[maxP.x];
           for (int l = 0 ; l < id.height; l++) {
               id.getPixels(0, l, maxP.x, buffer, 0);
               croppedId.setPixels(0, l, maxP.x, buffer, 0);
           }

           id = croppedId;
        }

        // apply the zoom
        if (factor != 1) {
            id = id.scaledTo(id.width / factor, id.height / factor);
        }

        return id;
    }

    /**
     * Convert the raw heap data to an image.  We know we're running in
     * the UI thread, so we can issue graphics commands directly.
     *
     * http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv/reference/api/org/eclipse/swt/graphics/GC.html
     *
     * @param cd The client data
     * @param mode The display mode. 0 = linear, 1 = hilbert.
     * @param forceRedraw
     */
    private void renderHeapData(ClientData cd, int mode, boolean forceRedraw) {
        Image image;

        byte[] pixData;

        // Atomically get and clear the heap data.
        synchronized (cd) {
            if (serializeHeapData(cd.getVmHeapData()) == false && forceRedraw == false) {
                // no change, we return.
                return;
            }

            pixData = getSerializedData();
        }

        if (pixData != null) {
            ImageData id;
            if (mode == 1) {
                id = createHilbertHeapImage(pixData);
            } else {
                id = createLinearHeapImage(pixData, 200, mMapPalette);
            }

            image = new Image(mDisplay, id);
        } else {
            // Render a placeholder image.
            int width, height;
            if (mode == 1) {
                width = height = PLACEHOLDER_HILBERT_SIZE;
            } else {
                width = PLACEHOLDER_LINEAR_H_SIZE;
                height = PLACEHOLDER_LINEAR_V_SIZE;
            }
            image = new Image(mDisplay, width, height);
            GC gc = new GC(image);
            gc.setForeground(mDisplay.getSystemColor(SWT.COLOR_RED));
            gc.drawLine(0, 0, width-1, height-1);
            gc.dispose();
            gc = null;
        }

        // set the new image

        if (mode == 1) {
            if (mHilbertImage != null) {
                mHilbertImage.dispose();
            }

            mHilbertImage = image;
            mHilbertHeapImage.setImage(mHilbertImage);
            mHilbertHeapImage.pack(true);
            mHilbertBase.layout();
            mHilbertBase.pack(true);
        } else {
            if (mLinearImage != null) {
                mLinearImage.dispose();
            }

            mLinearImage = image;
            mLinearHeapImage.setImage(mLinearImage);
            mLinearHeapImage.pack(true);
            mLinearBase.layout();
            mLinearBase.pack(true);
        }
    }

    @Override
    protected void setTableFocusListener() {
        addTableToFocusListener(mHeapSummary);
    }
}

