/*
 * 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 com.android.ddmlib.NativeAllocationInfo;
import com.android.ddmlib.NativeLibraryMapInfo;
import com.android.ddmlib.NativeStackCallInfo;
import com.android.ddmuilib.annotation.WorkerThread;

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.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
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.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Sash;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

/**
 * Panel with native heap information.
 */
public final class NativeHeapPanel extends BaseHeapPanel {

    /** 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 int ALLOC_DISPLAY_ALL = 0;
    private static final int ALLOC_DISPLAY_PRE_ZYGOTE = 1;
    private static final int ALLOC_DISPLAY_POST_ZYGOTE = 2;

    private Display mDisplay;

    private Composite mBase;

    private Label mUpdateStatus;

    /** combo giving choice of what to display: all, pre-zygote, post-zygote */
    private Combo mAllocDisplayCombo;

    private Button mFullUpdateButton;

    // see CreateControl()
    //private Button mDiffUpdateButton;

    private Combo mDisplayModeCombo;

    /** stack composite for mode (1-2) & 3 */
    private Composite mTopStackComposite;

    private StackLayout mTopStackLayout;

    /** stack composite for mode 1 & 2 */
    private Composite mAllocationStackComposite;

    private StackLayout mAllocationStackLayout;

    /** top level container for mode 1 & 2 */
    private Composite mTableModeControl;

    /** top level object for the allocation mode */
    private Control mAllocationModeTop;

    /** top level for the library mode */
    private Control mLibraryModeTopControl;

    /** composite for page UI and total memory display */
    private Composite mPageUIComposite;

    private Label mTotalMemoryLabel;

    private Label mPageLabel;

    private Button mPageNextButton;

    private Button mPagePreviousButton;

    private Table mAllocationTable;

    private Table mLibraryTable;

    private Table mLibraryAllocationTable;

    private Table mDetailTable;

    private Label mImage;

    private int mAllocDisplayMode = ALLOC_DISPLAY_ALL;

    /**
     * pointer to current stackcall thread computation in order to quit it if
     * required (new update requested)
     */
    private StackCallThread mStackCallThread;

    /** Current Library Allocation table fill thread. killed if selection changes */
    private FillTableThread mFillTableThread;

    /**
     * current client data. Used to access the malloc info when switching pages
     * or selecting allocation to show stack call
     */
    private ClientData mClientData;

    /**
     * client data from a previous display. used when asking for an "update & diff"
     */
    private ClientData mBackUpClientData;

    /** list of NativeAllocationInfo objects filled with the list from ClientData */
    private final ArrayList<NativeAllocationInfo> mAllocations =
        new ArrayList<NativeAllocationInfo>();

    /** list of the {@link NativeAllocationInfo} being displayed based on the selection
     * of {@link #mAllocDisplayCombo}.
     */
    private final ArrayList<NativeAllocationInfo> mDisplayedAllocations =
        new ArrayList<NativeAllocationInfo>();

    /** list of NativeAllocationInfo object kept as backup when doing an "update & diff" */
    private final ArrayList<NativeAllocationInfo> mBackUpAllocations =
        new ArrayList<NativeAllocationInfo>();

    /** back up of the total memory, used when doing an "update & diff" */
    private int mBackUpTotalMemory;

    private int mCurrentPage = 0;

    private int mPageCount = 0;

    /**
     * list of allocation per Library. This is created from the list of
     * NativeAllocationInfo objects that is stored in the ClientData object. Since we
     * don't keep this list around, it is recomputed everytime the client
     * changes.
     */
    private final ArrayList<LibraryAllocations> mLibraryAllocations =
        new ArrayList<LibraryAllocations>();

    /* args to setUpdateStatus() */
    private static final int NOT_SELECTED = 0;

    private static final int NOT_ENABLED = 1;

    private static final int ENABLED = 2;

    private static final int DISPLAY_PER_PAGE = 20;

    private static final String PREFS_ALLOCATION_SASH = "NHallocSash"; //$NON-NLS-1$
    private static final String PREFS_LIBRARY_SASH = "NHlibrarySash"; //$NON-NLS-1$
    private static final String PREFS_DETAIL_ADDRESS = "NHdetailAddress"; //$NON-NLS-1$
    private static final String PREFS_DETAIL_LIBRARY = "NHdetailLibrary"; //$NON-NLS-1$
    private static final String PREFS_DETAIL_METHOD = "NHdetailMethod"; //$NON-NLS-1$
    private static final String PREFS_DETAIL_FILE = "NHdetailFile"; //$NON-NLS-1$
    private static final String PREFS_DETAIL_LINE = "NHdetailLine"; //$NON-NLS-1$
    private static final String PREFS_ALLOC_TOTAL = "NHallocTotal"; //$NON-NLS-1$
    private static final String PREFS_ALLOC_COUNT = "NHallocCount"; //$NON-NLS-1$
    private static final String PREFS_ALLOC_SIZE = "NHallocSize"; //$NON-NLS-1$
    private static final String PREFS_ALLOC_LIBRARY = "NHallocLib"; //$NON-NLS-1$
    private static final String PREFS_ALLOC_METHOD = "NHallocMethod"; //$NON-NLS-1$
    private static final String PREFS_ALLOC_FILE = "NHallocFile"; //$NON-NLS-1$
    private static final String PREFS_LIB_LIBRARY = "NHlibLibrary"; //$NON-NLS-1$
    private static final String PREFS_LIB_SIZE = "NHlibSize"; //$NON-NLS-1$
    private static final String PREFS_LIB_COUNT = "NHlibCount"; //$NON-NLS-1$
    private static final String PREFS_LIBALLOC_TOTAL = "NHlibAllocTotal"; //$NON-NLS-1$
    private static final String PREFS_LIBALLOC_COUNT = "NHlibAllocCount"; //$NON-NLS-1$
    private static final String PREFS_LIBALLOC_SIZE = "NHlibAllocSize"; //$NON-NLS-1$
    private static final String PREFS_LIBALLOC_METHOD = "NHlibAllocMethod"; //$NON-NLS-1$

    /** static formatter object to format all numbers as #,### */
    private static DecimalFormat sFormatter;
    static {
        sFormatter = (DecimalFormat)NumberFormat.getInstance();
        if (sFormatter == null) {
            sFormatter = new DecimalFormat("#,###");
        } else {
            sFormatter.applyPattern("#,###");
        }
    }


    /**
     * caching mechanism to avoid recomputing the backtrace for a particular
     * address several times.
     */
    private HashMap<Long, NativeStackCallInfo> mSourceCache =
        new HashMap<Long, NativeStackCallInfo>();
    private long mTotalSize;
    private Button mSaveButton;
    private Button mSymbolsButton;

    /**
     * thread class to convert the address call into method, file and line
     * number in the background.
     */
    private class StackCallThread extends BackgroundThread {
        private ClientData mClientData;

        public StackCallThread(ClientData cd) {
            mClientData = cd;
        }

        public ClientData getClientData() {
            return mClientData;
        }

        @Override
        public void run() {
            // loop through all the NativeAllocationInfo and init them
            Iterator<NativeAllocationInfo> iter = mAllocations.iterator();
            int total = mAllocations.size();
            int count = 0;
            while (iter.hasNext()) {

                if (isQuitting())
                    return;

                NativeAllocationInfo info = iter.next();
                if (info.isStackCallResolved() == false) {
                    final List<Long> list = info.getStackCallAddresses();
                    final int size = list.size();

                    ArrayList<NativeStackCallInfo> resolvedStackCall =
                        new ArrayList<NativeStackCallInfo>();

                    for (int i = 0; i < size; i++) {
                        long addr = list.get(i);

                        // first check if the addr has already been converted.
                        NativeStackCallInfo source = mSourceCache.get(addr);

                        // if not we convert it
                        if (source == null) {
                            source = sourceForAddr(addr);
                            mSourceCache.put(addr, source);
                        }

                        resolvedStackCall.add(source);
                    }

                    info.setResolvedStackCall(resolvedStackCall);
                }
                // after every DISPLAY_PER_PAGE we ask for a ui refresh, unless
                // we reach total, since we also do it after the loop
                // (only an issue in case we have a perfect number of page)
                count++;
                if ((count % DISPLAY_PER_PAGE) == 0 && count != total) {
                    if (updateNHAllocationStackCalls(mClientData, count) == false) {
                        // looks like the app is quitting, so we just
                        // stopped the thread
                        return;
                    }
                }
            }

            updateNHAllocationStackCalls(mClientData, count);
        }

        private NativeStackCallInfo sourceForAddr(long addr) {
            NativeLibraryMapInfo library = getLibraryFor(addr);

            if (library != null) {

                Addr2Line process = Addr2Line.getProcess(library);
                if (process != null) {
                    // remove the base of the library address
                    NativeStackCallInfo info = process.getAddress(addr);
                    if (info != null) {
                        return info;
                    }
                }
            }

            return new NativeStackCallInfo(addr,
                    library != null ? library.getLibraryName() : null,
                    Long.toHexString(addr),
                    "");
        }

        private NativeLibraryMapInfo getLibraryFor(long addr) {
            for (NativeLibraryMapInfo info : mClientData.getMappedNativeLibraries()) {
                if (info.isWithinLibrary(addr)) {
                    return info;
                }
            }

            Log.d("ddm-nativeheap", "Failed finding Library for " + Long.toHexString(addr));
            return null;
        }

        /**
         * update the Native Heap panel with the amount of allocation for which the
         * stack call has been computed. This is called from a non UI thread, but
         * will be executed in the UI thread.
         *
         * @param count the amount of allocation
         * @return false if the display was disposed and the update couldn't happen
         */
        private boolean updateNHAllocationStackCalls(final ClientData clientData, final int count) {
            if (mDisplay.isDisposed() == false) {
                mDisplay.asyncExec(new Runnable() {
                    public void run() {
                        updateAllocationStackCalls(clientData, count);
                    }
                });
                return true;
            }
            return false;
        }
    }

    private class FillTableThread extends BackgroundThread {
        private LibraryAllocations mLibAlloc;

        private int mMax;

        public FillTableThread(LibraryAllocations liballoc, int m) {
            mLibAlloc = liballoc;
            mMax = m;
        }

        @Override
        public void run() {
            for (int i = mMax; i > 0 && isQuitting() == false; i -= 10) {
                updateNHLibraryAllocationTable(mLibAlloc, mMax - i, mMax - i + 10);
            }
        }

        /**
         * updates the library allocation table in the Native Heap panel. This is
         * called from a non UI thread, but will be executed in the UI thread.
         *
         * @param liballoc the current library allocation object being displayed
         * @param start start index of items that need to be displayed
         * @param end end index of the items that need to be displayed
         */
        private void updateNHLibraryAllocationTable(final LibraryAllocations libAlloc,
                final int start, final int end) {
            if (mDisplay.isDisposed() == false) {
                mDisplay.asyncExec(new Runnable() {
                    public void run() {
                        updateLibraryAllocationTable(libAlloc, start, end);
                    }
                });
            }

        }
    }

    /** class to aggregate allocations per library */
    public static class LibraryAllocations {
        private String mLibrary;

        private final ArrayList<NativeAllocationInfo> mLibAllocations =
            new ArrayList<NativeAllocationInfo>();

        private int mSize;

        private int mCount;

        /** construct the aggregate object for a library */
        public LibraryAllocations(final String lib) {
            mLibrary = lib;
        }

        /** get the library name */
        public String getLibrary() {
            return mLibrary;
        }

        /** add a NativeAllocationInfo object to this aggregate object */
        public void addAllocation(NativeAllocationInfo info) {
            mLibAllocations.add(info);
        }

        /** get an iterator on the NativeAllocationInfo objects */
        public Iterator<NativeAllocationInfo> getAllocations() {
            return mLibAllocations.iterator();
        }

        /** get a NativeAllocationInfo object by index */
        public NativeAllocationInfo getAllocation(int index) {
            return mLibAllocations.get(index);
        }

        /** returns the NativeAllocationInfo object count */
        public int getAllocationSize() {
            return mLibAllocations.size();
        }

        /** returns the total allocation size */
        public int getSize() {
            return mSize;
        }

        /** returns the number of allocations */
        public int getCount() {
            return mCount;
        }

        /**
         * compute the allocation count and size for allocation objects added
         * through <code>addAllocation()</code>, and sort the objects by
         * total allocation size.
         */
        public void computeAllocationSizeAndCount() {
            mSize = 0;
            mCount = 0;
            for (NativeAllocationInfo info : mLibAllocations) {
                mCount += info.getAllocationCount();
                mSize += info.getAllocationCount() * info.getSize();
            }
            Collections.sort(mLibAllocations, new Comparator<NativeAllocationInfo>() {
                public int compare(NativeAllocationInfo o1, NativeAllocationInfo o2) {
                    return o2.getAllocationCount() * o2.getSize() -
                        o1.getAllocationCount() * o1.getSize();
                }
            });
        }
    }

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

        mDisplay = parent.getDisplay();

        mBase = new Composite(parent, SWT.NONE);
        GridLayout gl = new GridLayout(1, false);
        gl.horizontalSpacing = 0;
        gl.verticalSpacing = 0;
        mBase.setLayout(gl);
        mBase.setLayoutData(new GridData(GridData.FILL_BOTH));

        // composite for <update btn> <status>
        Composite tmp = new Composite(mBase, SWT.NONE);
        tmp.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        tmp.setLayout(gl = new GridLayout(2, false));
        gl.marginWidth = gl.marginHeight = 0;

        mFullUpdateButton = new Button(tmp, SWT.NONE);
        mFullUpdateButton.setText("Full Update");
        mFullUpdateButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                mBackUpClientData = null;
                mDisplayModeCombo.setEnabled(false);
                mSaveButton.setEnabled(false);
                emptyTables();
                // if we already have a stack call computation for this
                // client
                // we stop it
                if (mStackCallThread != null &&
                        mStackCallThread.getClientData() == mClientData) {
                    mStackCallThread.quit();
                    mStackCallThread = null;
                }
                mLibraryAllocations.clear();
                Client client = getCurrentClient();
                if (client != null) {
                    client.requestNativeHeapInformation();
                }
            }
        });

        mUpdateStatus = new Label(tmp, SWT.NONE);
        mUpdateStatus.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        // top layout for the combos and oter controls on the right.
        Composite top_layout = new Composite(mBase, SWT.NONE);
        top_layout.setLayout(gl = new GridLayout(4, false));
        gl.marginWidth = gl.marginHeight = 0;

        new Label(top_layout, SWT.NONE).setText("Show:");

        mAllocDisplayCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
        mAllocDisplayCombo.setLayoutData(new GridData(
                GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
        mAllocDisplayCombo.add("All Allocations");
        mAllocDisplayCombo.add("Pre-Zygote Allocations");
        mAllocDisplayCombo.add("Zygote Child Allocations (Z)");
        mAllocDisplayCombo.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                onAllocDisplayChange();
            }
        });
        mAllocDisplayCombo.select(0);

        // separator
        Label separator = new Label(top_layout, SWT.SEPARATOR | SWT.VERTICAL);
        GridData gd;
        separator.setLayoutData(gd = new GridData(
                GridData.VERTICAL_ALIGN_FILL | GridData.GRAB_VERTICAL));
        gd.heightHint = 0;
        gd.verticalSpan = 2;

        mSaveButton = new Button(top_layout, SWT.PUSH);
        mSaveButton.setText("Save...");
        mSaveButton.setEnabled(false);
        mSaveButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                FileDialog fileDialog = new FileDialog(mBase.getShell(), SWT.SAVE);

                fileDialog.setText("Save Allocations");
                fileDialog.setFileName("allocations.txt");

                String fileName = fileDialog.open();
                if (fileName != null) {
                    saveAllocations(fileName);
                }
            }
        });

        /*
         * TODO: either fix the diff mechanism or remove it altogether.
        mDiffUpdateButton = new Button(top_layout, SWT.NONE);
        mDiffUpdateButton.setText("Update && Diff");
        mDiffUpdateButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                // since this is an update and diff, we need to store the
                // current list
                // of mallocs
                mBackUpAllocations.clear();
                mBackUpAllocations.addAll(mAllocations);
                mBackUpClientData = mClientData;
                mBackUpTotalMemory = mClientData.getTotalNativeMemory();

                mDisplayModeCombo.setEnabled(false);
                emptyTables();
                // if we already have a stack call computation for this
                // client
                // we stop it
                if (mStackCallThread != null &&
                        mStackCallThread.getClientData() == mClientData) {
                    mStackCallThread.quit();
                    mStackCallThread = null;
                }
                mLibraryAllocations.clear();
                Client client = getCurrentClient();
                if (client != null) {
                    client.requestNativeHeapInformation();
                }
            }
        });
        */

        Label l = new Label(top_layout, SWT.NONE);
        l.setText("Display:");

        mDisplayModeCombo = new Combo(top_layout, SWT.DROP_DOWN | SWT.READ_ONLY);
        mDisplayModeCombo.setLayoutData(new GridData(
                GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL));
        mDisplayModeCombo.setItems(new String[] { "Allocation List", "By Libraries" });
        mDisplayModeCombo.select(0);
        mDisplayModeCombo.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                switchDisplayMode();
            }
        });
        mDisplayModeCombo.setEnabled(false);

        mSymbolsButton = new Button(top_layout, SWT.PUSH);
        mSymbolsButton.setText("Load Symbols");
        mSymbolsButton.setEnabled(false);


        // create a composite that will contains the actual content composites,
        // in stack mode layout.
        // This top level composite contains 2 other composites.
        // * one for both Allocations and Libraries mode
        // * one for flat mode (which is gone for now)

        mTopStackComposite = new Composite(mBase, SWT.NONE);
        mTopStackComposite.setLayout(mTopStackLayout = new StackLayout());
        mTopStackComposite.setLayoutData(new GridData(GridData.FILL_BOTH));

        // create 1st and 2nd modes
        createTableDisplay(mTopStackComposite);

        mTopStackLayout.topControl = mTableModeControl;
        mTopStackComposite.layout();

        setUpdateStatus(NOT_SELECTED);

        // Work in progress
        // TODO add image display of native heap.
        //mImage = new Label(mBase, SWT.NONE);

        mBase.pack();

        return mBase;
    }

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


    /**
     * 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)
     */
    public void clientChanged(final Client client, int changeMask) {
        if (client == getCurrentClient()) {
            if ((changeMask & Client.CHANGE_NATIVE_HEAP_DATA) == Client.CHANGE_NATIVE_HEAP_DATA) {
                if (mBase.isDisposed())
                    return;

                mBase.getDisplay().asyncExec(new Runnable() {
                    public void run() {
                        clientSelected();
                    }
                });
            }
        }
    }

    /**
     * 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 (mBase.isDisposed())
            return;

        Client client = getCurrentClient();

        mDisplayModeCombo.setEnabled(false);
        emptyTables();

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

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

            // if (cd.getShowHeapUpdates())
            setUpdateStatus(ENABLED);
            // else
            // setUpdateStatus(NOT_ENABLED);

            initAllocationDisplay();

            //renderBitmap(cd);
        } else {
            mClientData = null;
            setUpdateStatus(NOT_SELECTED);
        }

        mBase.pack();
    }

    /**
     * Update the UI with the newly compute stack calls, unless the UI switched
     * to a different client.
     *
     * @param cd the ClientData for which the stack call are being computed.
     * @param count the current count of allocations for which the stack calls
     *            have been computed.
     */
    @WorkerThread
    public void updateAllocationStackCalls(ClientData cd, int count) {
        // we have to check that the panel still shows the same clientdata than
        // the thread is computing for.
        if (cd == mClientData) {

            int total = mAllocations.size();

            if (count == total) {
                // we're done: do something
                mDisplayModeCombo.setEnabled(true);
                mSaveButton.setEnabled(true);

                mStackCallThread = null;
            } else {
                // work in progress, update the progress bar.
//                mUiThread.setStatusLine("Computing stack call: " + count
//                        + "/" + total);
            }

            // FIXME: attempt to only update when needed.
            // Because the number of pages is not related to mAllocations.size() anymore
            // due to pre-zygote/post-zygote display, update all the time.
            // At some point we should remove the pages anyway, since it's getting computed
            // really fast now.
//            if ((mCurrentPage + 1) * DISPLAY_PER_PAGE == count
//                    || (count == total && mCurrentPage == mPageCount - 1)) {
            try {
                // get the current selection of the allocation
                int index = mAllocationTable.getSelectionIndex();
                NativeAllocationInfo info = null;

                if (index != -1) {
                    info = (NativeAllocationInfo)mAllocationTable.getItem(index).getData();
                }

                // empty the table
                emptyTables();

                // fill it again
                fillAllocationTable();

                // reselect
                mAllocationTable.setSelection(index);

                // display detail table if needed
                if (info != null) {
                    fillDetailTable(info);
                }
            } catch (SWTException e) {
                if (mAllocationTable.isDisposed()) {
                    // looks like the table is disposed. Let's ignore it.
                } else {
                    throw e;
                }
            }

        } else {
            // old client still running. doesn't really matter.
        }
    }

    @Override
    protected void setTableFocusListener() {
        addTableToFocusListener(mAllocationTable);
        addTableToFocusListener(mLibraryTable);
        addTableToFocusListener(mLibraryAllocationTable);
        addTableToFocusListener(mDetailTable);
    }

    protected void onAllocDisplayChange() {
        mAllocDisplayMode = mAllocDisplayCombo.getSelectionIndex();

        // create the new list
        updateAllocDisplayList();

        updateTotalMemoryDisplay();

        // reset the ui.
        mCurrentPage = 0;
        updatePageUI();
        switchDisplayMode();
    }

    private void updateAllocDisplayList() {
        mTotalSize = 0;
        mDisplayedAllocations.clear();
        for (NativeAllocationInfo info : mAllocations) {
            if (mAllocDisplayMode == ALLOC_DISPLAY_ALL ||
                    (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild())) {
                mDisplayedAllocations.add(info);
                mTotalSize += info.getSize() * info.getAllocationCount();
            } else {
                // skip this item
                continue;
            }
        }

        int count = mDisplayedAllocations.size();

        mPageCount = count / DISPLAY_PER_PAGE;

        // need to add a page for the rest of the div
        if ((count % DISPLAY_PER_PAGE) > 0) {
            mPageCount++;
        }
    }

    private void updateTotalMemoryDisplay() {
        switch (mAllocDisplayMode) {
            case ALLOC_DISPLAY_ALL:
                mTotalMemoryLabel.setText(String.format("Total Memory: %1$s Bytes",
                        sFormatter.format(mTotalSize)));
                break;
            case ALLOC_DISPLAY_PRE_ZYGOTE:
                mTotalMemoryLabel.setText(String.format("Zygote Memory: %1$s Bytes",
                        sFormatter.format(mTotalSize)));
                break;
            case ALLOC_DISPLAY_POST_ZYGOTE:
                mTotalMemoryLabel.setText(String.format("Post-zygote Memory: %1$s Bytes",
                        sFormatter.format(mTotalSize)));
                break;
        }
    }


    private void switchDisplayMode() {
        switch (mDisplayModeCombo.getSelectionIndex()) {
            case 0: {// allocations
                mTopStackLayout.topControl = mTableModeControl;
                mAllocationStackLayout.topControl = mAllocationModeTop;
                mAllocationStackComposite.layout();
                mTopStackComposite.layout();
                emptyTables();
                fillAllocationTable();
            }
                break;
            case 1: {// libraries
                mTopStackLayout.topControl = mTableModeControl;
                mAllocationStackLayout.topControl = mLibraryModeTopControl;
                mAllocationStackComposite.layout();
                mTopStackComposite.layout();
                emptyTables();
                fillLibraryTable();
            }
                break;
        }
    }

    private void initAllocationDisplay() {
        if (mStackCallThread != null) {
            mStackCallThread.quit();
        }

        mAllocations.clear();
        mAllocations.addAll(mClientData.getNativeAllocationList());

        updateAllocDisplayList();

        // if we have a previous clientdata and it matches the current one. we
        // do a diff between the new list and the old one.
        if (mBackUpClientData != null && mBackUpClientData == mClientData) {

            ArrayList<NativeAllocationInfo> add = new ArrayList<NativeAllocationInfo>();

            // we go through the list of NativeAllocationInfo in the new list and check if
            // there's one with the same exact data (size, allocation, count and
            // stackcall addresses) in the old list.
            // if we don't find any, we add it to the "add" list
            for (NativeAllocationInfo mi : mAllocations) {
                boolean found = false;
                for (NativeAllocationInfo old_mi : mBackUpAllocations) {
                    if (mi.equals(old_mi)) {
                        found = true;
                        break;
                    }
                }
                if (found == false) {
                    add.add(mi);
                }
            }

            // put the result in mAllocations
            mAllocations.clear();
            mAllocations.addAll(add);

            // display the difference in memory usage. This is computed
            // calculating the memory usage of the objects in mAllocations.
            int count = 0;
            for (NativeAllocationInfo allocInfo : mAllocations) {
                count += allocInfo.getSize() * allocInfo.getAllocationCount();
            }

            mTotalMemoryLabel.setText(String.format("Memory Difference: %1$s Bytes",
                    sFormatter.format(count)));
        }
        else {
            // display the full memory usage
            updateTotalMemoryDisplay();
            //mDiffUpdateButton.setEnabled(mClientData.getTotalNativeMemory() > 0);
        }
        mTotalMemoryLabel.pack();

        // update the page ui
        mDisplayModeCombo.select(0);

        mLibraryAllocations.clear();

        // reset to first page
        mCurrentPage = 0;

        // update the label
        updatePageUI();

        // now fill the allocation Table with the current page
        switchDisplayMode();

        // start the thread to compute the stack calls
        if (mAllocations.size() > 0) {
            mStackCallThread = new StackCallThread(mClientData);
            mStackCallThread.start();
        }
    }

    private void updatePageUI() {

        // set the label and pack to update the layout, otherwise
        // the label will be cut off if the new size is bigger
        if (mPageCount == 0) {
            mPageLabel.setText("0 of 0 allocations.");
        } else {
            StringBuffer buffer = new StringBuffer();
            // get our starting index
            int start = (mCurrentPage * DISPLAY_PER_PAGE) + 1;
            // end index, taking into account the last page can be half full
            int count = mDisplayedAllocations.size();
            int end = Math.min(start + DISPLAY_PER_PAGE - 1, count);
            buffer.append(sFormatter.format(start));
            buffer.append(" - ");
            buffer.append(sFormatter.format(end));
            buffer.append(" of ");
            buffer.append(sFormatter.format(count));
            buffer.append(" allocations.");
            mPageLabel.setText(buffer.toString());
        }

        // handle the button enabled state.
        mPagePreviousButton.setEnabled(mCurrentPage > 0);
        // reminder: mCurrentPage starts at 0.
        mPageNextButton.setEnabled(mCurrentPage < mPageCount - 1);

        mPageLabel.pack();
        mPageUIComposite.pack();

    }

    private void fillAllocationTable() {
        // get the count
        int count = mDisplayedAllocations.size();

        // get our starting index
        int start = mCurrentPage * DISPLAY_PER_PAGE;

        // loop for DISPLAY_PER_PAGE or till we reach count
        int end = start + DISPLAY_PER_PAGE;

        for (int i = start; i < end && i < count; i++) {
            NativeAllocationInfo info = mDisplayedAllocations.get(i);

            TableItem item = null;

            if (mAllocDisplayMode == ALLOC_DISPLAY_ALL)  {
                item = new TableItem(mAllocationTable, SWT.NONE);
                item.setText(0, (info.isZygoteChild() ? "Z " : "") +
                        sFormatter.format(info.getSize() * info.getAllocationCount()));
                item.setText(1, sFormatter.format(info.getAllocationCount()));
                item.setText(2, sFormatter.format(info.getSize()));
            } else if (mAllocDisplayMode == ALLOC_DISPLAY_PRE_ZYGOTE ^ info.isZygoteChild()) {
                item = new TableItem(mAllocationTable, SWT.NONE);
                item.setText(0, sFormatter.format(info.getSize() * info.getAllocationCount()));
                item.setText(1, sFormatter.format(info.getAllocationCount()));
                item.setText(2, sFormatter.format(info.getSize()));
            } else {
                // skip this item
                continue;
            }

            item.setData(info);

            NativeStackCallInfo bti = info.getRelevantStackCallInfo();
            if (bti != null) {
                String lib = bti.getLibraryName();
                String method = bti.getMethodName();
                String source = bti.getSourceFile();
                if (lib != null)
                    item.setText(3, lib);
                if (method != null)
                    item.setText(4, method);
                if (source != null)
                    item.setText(5, source);
            }
        }
    }

    private void fillLibraryTable() {
        // fill the library table
        sortAllocationsPerLibrary();

        for (LibraryAllocations liballoc : mLibraryAllocations) {
            if (liballoc != null) {
                TableItem item = new TableItem(mLibraryTable, SWT.NONE);
                String lib = liballoc.getLibrary();
                item.setText(0, lib != null ? lib : "");
                item.setText(1, sFormatter.format(liballoc.getSize()));
                item.setText(2, sFormatter.format(liballoc.getCount()));
            }
        }
    }

    private void fillLibraryAllocationTable() {
        mLibraryAllocationTable.removeAll();
        mDetailTable.removeAll();
        int index = mLibraryTable.getSelectionIndex();
        if (index != -1) {
            LibraryAllocations liballoc = mLibraryAllocations.get(index);
            // start a thread that will fill table 10 at a time to keep the ui
            // responsive, but first we kill the previous one if there was one
            if (mFillTableThread != null) {
                mFillTableThread.quit();
            }
            mFillTableThread = new FillTableThread(liballoc,
                    liballoc.getAllocationSize());
            mFillTableThread.start();
        }
    }

    public void updateLibraryAllocationTable(LibraryAllocations liballoc,
            int start, int end) {
        try {
            if (mLibraryTable.isDisposed() == false) {
                int index = mLibraryTable.getSelectionIndex();
                if (index != -1) {
                    LibraryAllocations newliballoc = mLibraryAllocations.get(
                            index);
                    if (newliballoc == liballoc) {
                        int count = liballoc.getAllocationSize();
                        for (int i = start; i < end && i < count; i++) {
                            NativeAllocationInfo info = liballoc.getAllocation(i);

                            TableItem item = new TableItem(
                                    mLibraryAllocationTable, SWT.NONE);
                            item.setText(0, sFormatter.format(
                                    info.getSize() * info.getAllocationCount()));
                            item.setText(1, sFormatter.format(info.getAllocationCount()));
                            item.setText(2, sFormatter.format(info.getSize()));

                            NativeStackCallInfo stackCallInfo = info.getRelevantStackCallInfo();
                            if (stackCallInfo != null) {
                                item.setText(3, stackCallInfo.getMethodName());
                            }
                        }
                    } else {
                        // we should quit the thread
                        if (mFillTableThread != null) {
                            mFillTableThread.quit();
                            mFillTableThread = null;
                        }
                    }
                }
            }
        } catch (SWTException e) {
            Log.e("ddms", "error when updating the library allocation table");
        }
    }

    private void fillDetailTable(final NativeAllocationInfo mi) {
        mDetailTable.removeAll();
        mDetailTable.setRedraw(false);

        try {
            // populate the detail Table with the back trace
            List<Long> addresses = mi.getStackCallAddresses();
            List<NativeStackCallInfo> resolvedStackCall = mi.getResolvedStackCall();

            if (resolvedStackCall == null) {
                return;
            }

            for (int i = 0 ; i < resolvedStackCall.size(); i++) {
                if (addresses.get(i) == null || addresses.get(i).longValue() == 0) {
                    continue;
                }

                long addr = addresses.get(i).longValue();
                NativeStackCallInfo source = resolvedStackCall.get(i);

                TableItem item = new TableItem(mDetailTable, SWT.NONE);
                item.setText(0, String.format("%08x", addr)); //$NON-NLS-1$

                String libraryName = source.getLibraryName();
                String methodName = source.getMethodName();
                String sourceFile = source.getSourceFile();
                int lineNumber = source.getLineNumber();

                if (libraryName != null)
                    item.setText(1, libraryName);
                if (methodName != null)
                    item.setText(2, methodName);
                if (sourceFile != null)
                    item.setText(3, sourceFile);
                if (lineNumber != -1)
                    item.setText(4, Integer.toString(lineNumber));
            }
        } finally {
            mDetailTable.setRedraw(true);
        }
    }

    /*
     * Are updates enabled?
     */
    private void setUpdateStatus(int status) {
        switch (status) {
            case NOT_SELECTED:
                mUpdateStatus.setText("Select a client to see heap info");
                mAllocDisplayCombo.setEnabled(false);
                mFullUpdateButton.setEnabled(false);
                //mDiffUpdateButton.setEnabled(false);
                break;
            case NOT_ENABLED:
                mUpdateStatus.setText("Heap updates are " + "NOT ENABLED for this client");
                mAllocDisplayCombo.setEnabled(false);
                mFullUpdateButton.setEnabled(false);
                //mDiffUpdateButton.setEnabled(false);
                break;
            case ENABLED:
                mUpdateStatus.setText("Press 'Full Update' to retrieve " + "latest data");
                mAllocDisplayCombo.setEnabled(true);
                mFullUpdateButton.setEnabled(true);
                //mDiffUpdateButton.setEnabled(true);
                break;
            default:
                throw new RuntimeException();
        }

        mUpdateStatus.pack();
    }

    /**
     * Create the Table display. This includes a "detail" Table in the bottom
     * half and 2 modes in the top half: allocation Table and
     * library+allocations Tables.
     *
     * @param base the top parent to create the display into
     */
    private void createTableDisplay(Composite base) {
        final int minPanelWidth = 60;

        final IPreferenceStore prefs = DdmUiPreferences.getStore();

        // top level composite for mode 1 & 2
        mTableModeControl = new Composite(base, SWT.NONE);
        GridLayout gl = new GridLayout(1, false);
        gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
        mTableModeControl.setLayout(gl);
        mTableModeControl.setLayoutData(new GridData(GridData.FILL_BOTH));

        mTotalMemoryLabel = new Label(mTableModeControl, SWT.NONE);
        mTotalMemoryLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        mTotalMemoryLabel.setText("Total Memory: 0 Bytes");

        // the top half of these modes is dynamic

        final Composite sash_composite = new Composite(mTableModeControl,
                SWT.NONE);
        sash_composite.setLayout(new FormLayout());
        sash_composite.setLayoutData(new GridData(GridData.FILL_BOTH));

        // create the stacked composite
        mAllocationStackComposite = new Composite(sash_composite, SWT.NONE);
        mAllocationStackLayout = new StackLayout();
        mAllocationStackComposite.setLayout(mAllocationStackLayout);
        mAllocationStackComposite.setLayoutData(new GridData(
                GridData.FILL_BOTH));

        // create the top half for mode 1
        createAllocationTopHalf(mAllocationStackComposite);

        // create the top half for mode 2
        createLibraryTopHalf(mAllocationStackComposite);

        final Sash sash = new Sash(sash_composite, SWT.HORIZONTAL);

        // bottom half of these modes is the same: detail table
        createDetailTable(sash_composite);

        // init value for stack
        mAllocationStackLayout.topControl = mAllocationModeTop;

        // form layout data
        FormData data = new FormData();
        data.top = new FormAttachment(mTotalMemoryLabel, 0);
        data.bottom = new FormAttachment(sash, 0);
        data.left = new FormAttachment(0, 0);
        data.right = new FormAttachment(100, 0);
        mAllocationStackComposite.setLayoutData(data);

        final FormData sashData = new FormData();
        if (prefs != null && prefs.contains(PREFS_ALLOCATION_SASH)) {
            sashData.top = new FormAttachment(0,
                    prefs.getInt(PREFS_ALLOCATION_SASH));
        } else {
            sashData.top = new FormAttachment(50, 0); // 50% across
        }
        sashData.left = new FormAttachment(0, 0);
        sashData.right = new FormAttachment(100, 0);
        sash.setLayoutData(sashData);

        data = new FormData();
        data.top = new FormAttachment(sash, 0);
        data.bottom = new FormAttachment(100, 0);
        data.left = new FormAttachment(0, 0);
        data.right = new FormAttachment(100, 0);
        mDetailTable.setLayoutData(data);

        // allow resizes, but cap at minPanelWidth
        sash.addListener(SWT.Selection, new Listener() {
            public void handleEvent(Event e) {
                Rectangle sashRect = sash.getBounds();
                Rectangle panelRect = sash_composite.getClientArea();
                int bottom = panelRect.height - sashRect.height - minPanelWidth;
                e.y = Math.max(Math.min(e.y, bottom), minPanelWidth);
                if (e.y != sashRect.y) {
                    sashData.top = new FormAttachment(0, e.y);
                    prefs.setValue(PREFS_ALLOCATION_SASH, e.y);
                    sash_composite.layout();
                }
            }
        });
    }

    private void createDetailTable(Composite base) {

        final IPreferenceStore prefs = DdmUiPreferences.getStore();

        mDetailTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
        mDetailTable.setLayoutData(new GridData(GridData.FILL_BOTH));
        mDetailTable.setHeaderVisible(true);
        mDetailTable.setLinesVisible(true);

        TableHelper.createTableColumn(mDetailTable, "Address", SWT.RIGHT,
                "00000000", PREFS_DETAIL_ADDRESS, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mDetailTable, "Library", SWT.LEFT,
                "abcdefghijklmnopqrst", PREFS_DETAIL_LIBRARY, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mDetailTable, "Method", SWT.LEFT,
                "abcdefghijklmnopqrst", PREFS_DETAIL_METHOD, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mDetailTable, "File", SWT.LEFT,
                "abcdefghijklmnopqrstuvwxyz", PREFS_DETAIL_FILE, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mDetailTable, "Line", SWT.RIGHT,
                "9,999", PREFS_DETAIL_LINE, prefs); //$NON-NLS-1$
    }

    private void createAllocationTopHalf(Composite b) {
        final IPreferenceStore prefs = DdmUiPreferences.getStore();

        Composite base = new Composite(b, SWT.NONE);
        mAllocationModeTop = base;
        GridLayout gl = new GridLayout(1, false);
        gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
        gl.verticalSpacing = 0;
        base.setLayout(gl);
        base.setLayoutData(new GridData(GridData.FILL_BOTH));

        // horizontal layout for memory total and pages UI
        mPageUIComposite = new Composite(base, SWT.NONE);
        mPageUIComposite.setLayoutData(new GridData(
                GridData.HORIZONTAL_ALIGN_BEGINNING));
        gl = new GridLayout(3, false);
        gl.marginLeft = gl.marginRight = gl.marginTop = gl.marginBottom = 0;
        gl.horizontalSpacing = 0;
        mPageUIComposite.setLayout(gl);

        // Page UI
        mPagePreviousButton = new Button(mPageUIComposite, SWT.NONE);
        mPagePreviousButton.setText("<");
        mPagePreviousButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                mCurrentPage--;
                updatePageUI();
                emptyTables();
                fillAllocationTable();
            }
        });

        mPageNextButton = new Button(mPageUIComposite, SWT.NONE);
        mPageNextButton.setText(">");
        mPageNextButton.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                mCurrentPage++;
                updatePageUI();
                emptyTables();
                fillAllocationTable();
            }
        });

        mPageLabel = new Label(mPageUIComposite, SWT.NONE);
        mPageLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        updatePageUI();

        mAllocationTable = new Table(base, SWT.MULTI | SWT.FULL_SELECTION);
        mAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
        mAllocationTable.setHeaderVisible(true);
        mAllocationTable.setLinesVisible(true);

        TableHelper.createTableColumn(mAllocationTable, "Total", SWT.RIGHT,
                "9,999,999", PREFS_ALLOC_TOTAL, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mAllocationTable, "Count", SWT.RIGHT,
                "9,999", PREFS_ALLOC_COUNT, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mAllocationTable, "Size", SWT.RIGHT,
                "999,999", PREFS_ALLOC_SIZE, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mAllocationTable, "Library", SWT.LEFT,
                "abcdefghijklmnopqrst", PREFS_ALLOC_LIBRARY, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mAllocationTable, "Method", SWT.LEFT,
                "abcdefghijklmnopqrst", PREFS_ALLOC_METHOD, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mAllocationTable, "File", SWT.LEFT,
                "abcdefghijklmnopqrstuvwxyz", PREFS_ALLOC_FILE, prefs); //$NON-NLS-1$

        mAllocationTable.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                // get the selection index
                int index = mAllocationTable.getSelectionIndex();
                if (index >= 0 && index < mAllocationTable.getItemCount()) {
                    TableItem item = mAllocationTable.getItem(index);
                    if (item != null && item.getData() instanceof NativeAllocationInfo) {
                        fillDetailTable((NativeAllocationInfo)item.getData());
                    }
                }
            }
        });
    }

    private void createLibraryTopHalf(Composite base) {
        final int minPanelWidth = 60;

        final IPreferenceStore prefs = DdmUiPreferences.getStore();

        // create a composite that'll contain 2 tables horizontally
        final Composite top = new Composite(base, SWT.NONE);
        mLibraryModeTopControl = top;
        top.setLayout(new FormLayout());
        top.setLayoutData(new GridData(GridData.FILL_BOTH));

        // first table: library
        mLibraryTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
        mLibraryTable.setLayoutData(new GridData(GridData.FILL_BOTH));
        mLibraryTable.setHeaderVisible(true);
        mLibraryTable.setLinesVisible(true);

        TableHelper.createTableColumn(mLibraryTable, "Library", SWT.LEFT,
                "abcdefghijklmnopqrstuvwxyz", PREFS_LIB_LIBRARY, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mLibraryTable, "Size", SWT.RIGHT,
                "9,999,999", PREFS_LIB_SIZE, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mLibraryTable, "Count", SWT.RIGHT,
                "9,999", PREFS_LIB_COUNT, prefs); //$NON-NLS-1$

        mLibraryTable.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                fillLibraryAllocationTable();
            }
        });

        final Sash sash = new Sash(top, SWT.VERTICAL);

        // 2nd table: allocation per library
        mLibraryAllocationTable = new Table(top, SWT.MULTI | SWT.FULL_SELECTION);
        mLibraryAllocationTable.setLayoutData(new GridData(GridData.FILL_BOTH));
        mLibraryAllocationTable.setHeaderVisible(true);
        mLibraryAllocationTable.setLinesVisible(true);

        TableHelper.createTableColumn(mLibraryAllocationTable, "Total",
                SWT.RIGHT, "9,999,999", PREFS_LIBALLOC_TOTAL, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mLibraryAllocationTable, "Count",
                SWT.RIGHT, "9,999", PREFS_LIBALLOC_COUNT, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mLibraryAllocationTable, "Size",
                SWT.RIGHT, "999,999", PREFS_LIBALLOC_SIZE, prefs); //$NON-NLS-1$
        TableHelper.createTableColumn(mLibraryAllocationTable, "Method",
                SWT.LEFT, "abcdefghijklmnopqrst", PREFS_LIBALLOC_METHOD, prefs); //$NON-NLS-1$

        mLibraryAllocationTable.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                // get the index of the selection in the library table
                int index1 = mLibraryTable.getSelectionIndex();
                // get the index in the library allocation table
                int index2 = mLibraryAllocationTable.getSelectionIndex();
                // get the MallocInfo object
                if (index1 != -1 && index2 != -1) {
                    LibraryAllocations liballoc = mLibraryAllocations.get(index1);
                    NativeAllocationInfo info = liballoc.getAllocation(index2);
                    fillDetailTable(info);
                }
            }
        });

        // form layout data
        FormData data = new FormData();
        data.top = new FormAttachment(0, 0);
        data.bottom = new FormAttachment(100, 0);
        data.left = new FormAttachment(0, 0);
        data.right = new FormAttachment(sash, 0);
        mLibraryTable.setLayoutData(data);

        final FormData sashData = new FormData();
        if (prefs != null && prefs.contains(PREFS_LIBRARY_SASH)) {
            sashData.left = new FormAttachment(0,
                    prefs.getInt(PREFS_LIBRARY_SASH));
        } else {
            sashData.left = new FormAttachment(50, 0);
        }
        sashData.bottom = new FormAttachment(100, 0);
        sashData.top = new FormAttachment(0, 0); // 50% across
        sash.setLayoutData(sashData);

        data = new FormData();
        data.top = new FormAttachment(0, 0);
        data.bottom = new FormAttachment(100, 0);
        data.left = new FormAttachment(sash, 0);
        data.right = new FormAttachment(100, 0);
        mLibraryAllocationTable.setLayoutData(data);

        // allow resizes, but cap at minPanelWidth
        sash.addListener(SWT.Selection, new Listener() {
            public void handleEvent(Event e) {
                Rectangle sashRect = sash.getBounds();
                Rectangle panelRect = top.getClientArea();
                int right = panelRect.width - sashRect.width - minPanelWidth;
                e.x = Math.max(Math.min(e.x, right), minPanelWidth);
                if (e.x != sashRect.x) {
                    sashData.left = new FormAttachment(0, e.x);
                    prefs.setValue(PREFS_LIBRARY_SASH, e.y);
                    top.layout();
                }
            }
        });
    }

    private void emptyTables() {
        mAllocationTable.removeAll();
        mLibraryTable.removeAll();
        mLibraryAllocationTable.removeAll();
        mDetailTable.removeAll();
    }

    private void sortAllocationsPerLibrary() {
        if (mClientData != null) {
            mLibraryAllocations.clear();

            // create a hash map of LibraryAllocations to access aggregate
            // objects already created
            HashMap<String, LibraryAllocations> libcache =
                new HashMap<String, LibraryAllocations>();

            // get the allocation count
            int count = mDisplayedAllocations.size();
            for (int i = 0; i < count; i++) {
                NativeAllocationInfo allocInfo = mDisplayedAllocations.get(i);

                NativeStackCallInfo stackCallInfo = allocInfo.getRelevantStackCallInfo();
                if (stackCallInfo != null) {
                    String libraryName = stackCallInfo.getLibraryName();
                    LibraryAllocations liballoc = libcache.get(libraryName);
                    if (liballoc == null) {
                        // didn't find a library allocation object already
                        // created so we create one
                        liballoc = new LibraryAllocations(libraryName);
                        // add it to the cache
                        libcache.put(libraryName, liballoc);
                        // add it to the list
                        mLibraryAllocations.add(liballoc);
                    }
                    // add the MallocInfo object to it.
                    liballoc.addAllocation(allocInfo);
                }
            }
            // now that the list is created, we need to compute the size and
            // sort it by size. This will also sort the MallocInfo objects
            // inside each LibraryAllocation objects.
            for (LibraryAllocations liballoc : mLibraryAllocations) {
                liballoc.computeAllocationSizeAndCount();
            }

            // now we sort it
            Collections.sort(mLibraryAllocations,
                    new Comparator<LibraryAllocations>() {
                public int compare(LibraryAllocations o1,
                        LibraryAllocations o2) {
                    return o2.getSize() - o1.getSize();
                }
            });
        }
    }

    private void renderBitmap(ClientData cd) {
        byte[] pixData;

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

            pixData = getSerializedData();

            ImageData id = createLinearHeapImage(pixData, 200, mMapPalette);
            Image image = new Image(mBase.getDisplay(), id);
            mImage.setImage(image);
            mImage.pack(true);
        }
    }

    /*
     * 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);
    }

    private void saveAllocations(String fileName) {
        try {
            PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(fileName)));

            for (NativeAllocationInfo alloc : mAllocations) {
                out.println(alloc.toString());
            }
            out.close();
        } catch (IOException e) {
            Log.e("Native", e);
        }
    }
}
