/*
 * Copyright (C) 2010 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.hierarchyviewerlib.ui;

import com.android.ddmuilib.ImageLoader;
import com.android.hierarchyviewerlib.HierarchyViewerDirector;
import com.android.hierarchyviewerlib.models.TreeViewModel;
import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
import com.android.hierarchyviewerlib.models.ViewNode.ProfileRating;
import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Point;
import com.android.hierarchyviewerlib.ui.util.DrawableViewNode.Rectangle;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
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.Path;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

import java.text.DecimalFormat;

public class TreeView extends Canvas implements ITreeChangeListener {

    private TreeViewModel mModel;

    private DrawableViewNode mTree;

    private DrawableViewNode mSelectedNode;

    private Rectangle mViewport;

    private Transform mTransform;

    private Transform mInverse;

    private double mZoom;

    private Point mLastPoint;

    private boolean mAlreadySelectedOnMouseDown;

    private boolean mDoubleClicked;

    private boolean mNodeMoved;

    private DrawableViewNode mDraggedNode;

    public static final int LINE_PADDING = 10;

    public static final float BEZIER_FRACTION = 0.35f;

    private static Image sRedImage;

    private static Image sYellowImage;

    private static Image sGreenImage;

    private static Image sNotSelectedImage;

    private static Image sSelectedImage;

    private static Image sFilteredImage;

    private static Image sFilteredSelectedImage;

    private static Font sSystemFont;

    private Color mBoxColor;

    private Color mTextBackgroundColor;

    private Rectangle mSelectedRectangleLocation;

    private Point mButtonCenter;

    private static final int BUTTON_SIZE = 13;

    private Image mScaledSelectedImage;

    private boolean mButtonClicked;

    private DrawableViewNode mLastDrawnSelectedViewNode;

    // The profile-image box needs to be moved to,
    // so add some dragging leeway.
    private static final int DRAG_LEEWAY = 220;

    // Profile-image box constants
    private static final int RECT_WIDTH = 190;

    private static final int RECT_HEIGHT = 224;

    private static final int BUTTON_RIGHT_OFFSET = 5;

    private static final int BUTTON_TOP_OFFSET = 5;

    private static final int IMAGE_WIDTH = 125;

    private static final int IMAGE_HEIGHT = 120;

    private static final int IMAGE_OFFSET = 6;

    private static final int IMAGE_ROUNDING = 8;

    private static final int RECTANGLE_SIZE = 5;

    private static final int TEXT_SIDE_OFFSET = 8;

    private static final int TEXT_TOP_OFFSET = 4;

    private static final int TEXT_SPACING = 2;

    private static final int TEXT_ROUNDING = 20;

    public TreeView(Composite parent) {
        super(parent, SWT.NONE);

        mModel = TreeViewModel.getModel();
        mModel.addTreeChangeListener(this);

        addPaintListener(mPaintListener);
        addMouseListener(mMouseListener);
        addMouseMoveListener(mMouseMoveListener);
        addMouseWheelListener(mMouseWheelListener);
        addListener(SWT.Resize, mResizeListener);
        addDisposeListener(mDisposeListener);
        addKeyListener(mKeyListener);
        addTraverseListener(new TraverseListener() {
            @Override
            public void keyTraversed(TraverseEvent e) {
                if (e.detail == SWT.TRAVERSE_TAB_NEXT || e.detail == SWT.TRAVERSE_TAB_PREVIOUS) {
                    e.doit = true;
                }
            }
        });

        loadResources();

        mTransform = new Transform(Display.getDefault());
        mInverse = new Transform(Display.getDefault());

        loadAllData();
    }

    private void loadResources() {
        ImageLoader loader = ImageLoader.getLoader(this.getClass());
        sRedImage = loader.loadImage("red.png", Display.getDefault()); //$NON-NLS-1$
        sYellowImage = loader.loadImage("yellow.png", Display.getDefault()); //$NON-NLS-1$
        sGreenImage = loader.loadImage("green.png", Display.getDefault()); //$NON-NLS-1$
        sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$
        sSelectedImage = loader.loadImage("selected.png", Display.getDefault()); //$NON-NLS-1$
        sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$
        sFilteredSelectedImage = loader.loadImage("selected-filtered.png", Display.getDefault()); //$NON-NLS-1$
        mBoxColor = new Color(Display.getDefault(), new RGB(225, 225, 225));
        mTextBackgroundColor = new Color(Display.getDefault(), new RGB(82, 82, 82));
        if (mScaledSelectedImage != null) {
            mScaledSelectedImage.dispose();
        }
        sSystemFont = Display.getDefault().getSystemFont();
    }

    private DisposeListener mDisposeListener = new DisposeListener() {
        @Override
        public void widgetDisposed(DisposeEvent e) {
            mModel.removeTreeChangeListener(TreeView.this);
            mTransform.dispose();
            mInverse.dispose();
            mBoxColor.dispose();
            mTextBackgroundColor.dispose();
            if (mTree != null) {
                mModel.setViewport(null);
            }
        }
    };

    private Listener mResizeListener = new Listener() {
        @Override
        public void handleEvent(Event e) {
            synchronized (TreeView.this) {
                if (mTree != null && mViewport != null) {

                    // Keep the center in the same place.
                    Point viewCenter =
                            new Point(mViewport.x + mViewport.width / 2, mViewport.y + mViewport.height
                                    / 2);
                    mViewport.width = getBounds().width / mZoom;
                    mViewport.height = getBounds().height / mZoom;
                    mViewport.x = viewCenter.x - mViewport.width / 2;
                    mViewport.y = viewCenter.y - mViewport.height / 2;
                }
            }
            if (mViewport != null) {
                mModel.setViewport(mViewport);
            }
        }
    };

    private KeyListener mKeyListener = new KeyListener() {

        @Override
        public void keyPressed(KeyEvent e) {
            boolean selectionChanged = false;
            DrawableViewNode clickedNode = null;
            synchronized (TreeView.this) {
                if (mTree != null && mViewport != null && mSelectedNode != null) {
                    switch (e.keyCode) {
                        case SWT.ARROW_LEFT:
                            if (mSelectedNode.parent != null) {
                                mSelectedNode = mSelectedNode.parent;
                                selectionChanged = true;
                            }
                            break;
                        case SWT.ARROW_UP:

                            // On up and down, it is cool to go up and down only
                            // the leaf nodes.
                            // It goes well with the layout viewer
                            DrawableViewNode currentNode = mSelectedNode;
                            while (currentNode.parent != null && currentNode.viewNode.index == 0) {
                                currentNode = currentNode.parent;
                            }
                            if (currentNode.parent != null) {
                                selectionChanged = true;
                                currentNode =
                                        currentNode.parent.children
                                                .get(currentNode.viewNode.index - 1);
                                while (currentNode.children.size() != 0) {
                                    currentNode =
                                            currentNode.children
                                                    .get(currentNode.children.size() - 1);
                                }
                            }
                            if (selectionChanged) {
                                mSelectedNode = currentNode;
                            }
                            break;
                        case SWT.ARROW_DOWN:
                            currentNode = mSelectedNode;
                            while (currentNode.parent != null
                                    && currentNode.viewNode.index + 1 == currentNode.parent.children
                                            .size()) {
                                currentNode = currentNode.parent;
                            }
                            if (currentNode.parent != null) {
                                selectionChanged = true;
                                currentNode =
                                        currentNode.parent.children
                                                .get(currentNode.viewNode.index + 1);
                                while (currentNode.children.size() != 0) {
                                    currentNode = currentNode.children.get(0);
                                }
                            }
                            if (selectionChanged) {
                                mSelectedNode = currentNode;
                            }
                            break;
                        case SWT.ARROW_RIGHT:
                            DrawableViewNode rightNode = null;
                            double mostOverlap = 0;
                            final int N = mSelectedNode.children.size();

                            // We consider all the children and pick the one
                            // who's tree overlaps the most.
                            for (int i = 0; i < N; i++) {
                                DrawableViewNode child = mSelectedNode.children.get(i);
                                DrawableViewNode topMostChild = child;
                                while (topMostChild.children.size() != 0) {
                                    topMostChild = topMostChild.children.get(0);
                                }
                                double overlap =
                                        Math.min(DrawableViewNode.NODE_HEIGHT, Math.min(
                                                mSelectedNode.top + DrawableViewNode.NODE_HEIGHT
                                                        - topMostChild.top, topMostChild.top
                                                        + child.treeHeight - mSelectedNode.top));
                                if (overlap > mostOverlap) {
                                    mostOverlap = overlap;
                                    rightNode = child;
                                }
                            }
                            if (rightNode != null) {
                                mSelectedNode = rightNode;
                                selectionChanged = true;
                            }
                            break;
                        case SWT.CR:
                            clickedNode = mSelectedNode;
                            break;
                    }
                }
            }
            if (selectionChanged) {
                mModel.setSelection(mSelectedNode);
            }
            if (clickedNode != null) {
                HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode);
            }
        }

        @Override
        public void keyReleased(KeyEvent e) {
        }
    };

    private MouseListener mMouseListener = new MouseListener() {

        @Override
        public void mouseDoubleClick(MouseEvent e) {
            DrawableViewNode clickedNode = null;
            synchronized (TreeView.this) {
                if (mTree != null && mViewport != null) {
                    Point pt = transformPoint(e.x, e.y);
                    clickedNode = mTree.getSelected(pt.x, pt.y);
                }
            }
            if (clickedNode != null) {
                HierarchyViewerDirector.getDirector().showCapture(getShell(), clickedNode.viewNode);
                mDoubleClicked = true;
            }
        }

        @Override
        public void mouseDown(MouseEvent e) {
            boolean selectionChanged = false;
            synchronized (TreeView.this) {
                if (mTree != null && mViewport != null) {
                    Point pt = transformPoint(e.x, e.y);

                    // Ignore profiling rectangle, except for...
                    if (mSelectedRectangleLocation != null
                            && pt.x >= mSelectedRectangleLocation.x
                            && pt.x < mSelectedRectangleLocation.x
                                    + mSelectedRectangleLocation.width
                            && pt.y >= mSelectedRectangleLocation.y
                            && pt.y < mSelectedRectangleLocation.y
                                    + mSelectedRectangleLocation.height) {

                        // the small button!
                        if ((pt.x - mButtonCenter.x) * (pt.x - mButtonCenter.x)
                                + (pt.y - mButtonCenter.y) * (pt.y - mButtonCenter.y) <= (BUTTON_SIZE * BUTTON_SIZE) / 4) {
                            mButtonClicked = true;
                            doRedraw();
                        }
                        return;
                    }
                    mDraggedNode = mTree.getSelected(pt.x, pt.y);

                    // Update the selection.
                    if (mDraggedNode != null && mDraggedNode != mSelectedNode) {
                        mSelectedNode = mDraggedNode;
                        selectionChanged = true;
                        mAlreadySelectedOnMouseDown = false;
                    } else if (mDraggedNode != null) {
                        mAlreadySelectedOnMouseDown = true;
                    }

                    // Can't drag the root.
                    if (mDraggedNode == mTree) {
                        mDraggedNode = null;
                    }

                    if (mDraggedNode != null) {
                        mLastPoint = pt;
                    } else {
                        mLastPoint = new Point(e.x, e.y);
                    }
                    mNodeMoved = false;
                    mDoubleClicked = false;
                }
            }
            if (selectionChanged) {
                mModel.setSelection(mSelectedNode);
            }
        }

        @Override
        public void mouseUp(MouseEvent e) {
            boolean redraw = false;
            boolean redrawButton = false;
            boolean viewportChanged = false;
            boolean selectionChanged = false;
            synchronized (TreeView.this) {
                if (mTree != null && mViewport != null && mLastPoint != null) {
                    if (mDraggedNode == null) {
                        // The viewport moves.
                        handleMouseDrag(new Point(e.x, e.y));
                        viewportChanged = true;
                    } else {
                        // The nodes move.
                        handleMouseDrag(transformPoint(e.x, e.y));
                    }

                    // Deselect on the second click...
                    // This is in the mouse up, because mouse up happens after a
                    // double click event.
                    // During a double click, we don't want to deselect.
                    Point pt = transformPoint(e.x, e.y);
                    DrawableViewNode mouseUpOn = mTree.getSelected(pt.x, pt.y);
                    if (mouseUpOn != null && mouseUpOn == mSelectedNode
                            && mAlreadySelectedOnMouseDown && !mNodeMoved && !mDoubleClicked) {
                        mSelectedNode = null;
                        selectionChanged = true;
                    }
                    mLastPoint = null;
                    mDraggedNode = null;
                    redraw = true;
                }

                // Just clicked the button here.
                if (mButtonClicked) {
                    HierarchyViewerDirector.getDirector().showCapture(getShell(),
                            mSelectedNode.viewNode);
                    mButtonClicked = false;
                    redrawButton = true;
                }
            }

            // Complicated.
            if (viewportChanged) {
                mModel.setViewport(mViewport);
            } else if (redraw) {
                mModel.removeTreeChangeListener(TreeView.this);
                mModel.notifyViewportChanged();
                if (selectionChanged) {
                    mModel.setSelection(mSelectedNode);
                }
                mModel.addTreeChangeListener(TreeView.this);
                doRedraw();
            } else if (redrawButton) {
                doRedraw();
            }
        }

    };

    private MouseMoveListener mMouseMoveListener = new MouseMoveListener() {
        @Override
        public void mouseMove(MouseEvent e) {
            boolean redraw = false;
            boolean viewportChanged = false;
            synchronized (TreeView.this) {
                if (mTree != null && mViewport != null && mLastPoint != null) {
                    if (mDraggedNode == null) {
                        handleMouseDrag(new Point(e.x, e.y));
                        viewportChanged = true;
                    } else {
                        handleMouseDrag(transformPoint(e.x, e.y));
                    }
                    redraw = true;
                }
            }
            if (viewportChanged) {
                mModel.setViewport(mViewport);
            } else if (redraw) {
                mModel.removeTreeChangeListener(TreeView.this);
                mModel.notifyViewportChanged();
                mModel.addTreeChangeListener(TreeView.this);
                doRedraw();
            }
        }
    };

    private void handleMouseDrag(Point pt) {

        // Case 1: a node is dragged. DrawableViewNode knows how to handle this.
        if (mDraggedNode != null) {
            if (mLastPoint.y - pt.y != 0) {
                mNodeMoved = true;
            }
            mDraggedNode.move(mLastPoint.y - pt.y);
            mLastPoint = pt;
            return;
        }

        // Case 2: the viewport is dragged. We have to make sure we respect the
        // bounds - don't let the user drag way out... + some leeway for the
        // profiling box.
        double xDif = (mLastPoint.x - pt.x) / mZoom;
        double yDif = (mLastPoint.y - pt.y) / mZoom;

        double treeX = mTree.bounds.x - DRAG_LEEWAY;
        double treeY = mTree.bounds.y - DRAG_LEEWAY;
        double treeWidth = mTree.bounds.width + 2 * DRAG_LEEWAY;
        double treeHeight = mTree.bounds.height + 2 * DRAG_LEEWAY;

        if (mViewport.width > treeWidth) {
            if (xDif < 0 && mViewport.x + mViewport.width > treeX + treeWidth) {
                mViewport.x = Math.max(mViewport.x + xDif, treeX + treeWidth - mViewport.width);
            } else if (xDif > 0 && mViewport.x < treeX) {
                mViewport.x = Math.min(mViewport.x + xDif, treeX);
            }
        } else {
            if (xDif < 0 && mViewport.x > treeX) {
                mViewport.x = Math.max(mViewport.x + xDif, treeX);
            } else if (xDif > 0 && mViewport.x + mViewport.width < treeX + treeWidth) {
                mViewport.x = Math.min(mViewport.x + xDif, treeX + treeWidth - mViewport.width);
            }
        }
        if (mViewport.height > treeHeight) {
            if (yDif < 0 && mViewport.y + mViewport.height > treeY + treeHeight) {
                mViewport.y = Math.max(mViewport.y + yDif, treeY + treeHeight - mViewport.height);
            } else if (yDif > 0 && mViewport.y < treeY) {
                mViewport.y = Math.min(mViewport.y + yDif, treeY);
            }
        } else {
            if (yDif < 0 && mViewport.y > treeY) {
                mViewport.y = Math.max(mViewport.y + yDif, treeY);
            } else if (yDif > 0 && mViewport.y + mViewport.height < treeY + treeHeight) {
                mViewport.y = Math.min(mViewport.y + yDif, treeY + treeHeight - mViewport.height);
            }
        }
        mLastPoint = pt;
    }

    private Point transformPoint(double x, double y) {
        float[] pt = {
                (float) x, (float) y
        };
        mInverse.transform(pt);
        return new Point(pt[0], pt[1]);
    }

    private MouseWheelListener mMouseWheelListener = new MouseWheelListener() {
        @Override
        public void mouseScrolled(MouseEvent e) {
            Point zoomPoint = null;
            synchronized (TreeView.this) {
                if (mTree != null && mViewport != null) {
                    mZoom += Math.ceil(e.count / 3.0) * 0.1;
                    zoomPoint = transformPoint(e.x, e.y);
                }
            }
            if (zoomPoint != null) {
                mModel.zoomOnPoint(mZoom, zoomPoint);
            }
        }
    };

    private PaintListener mPaintListener = new PaintListener() {
        @Override
        public void paintControl(PaintEvent e) {
            synchronized (TreeView.this) {
                e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
                e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
                if (mTree != null && mViewport != null) {

                    // Easy stuff!
                    e.gc.setTransform(mTransform);
                    e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
                    Path connectionPath = new Path(Display.getDefault());
                    paintRecursive(e.gc, mTransform, mTree, mSelectedNode, connectionPath);
                    e.gc.drawPath(connectionPath);
                    connectionPath.dispose();

                    // Draw the profiling box.
                    if (mSelectedNode != null) {

                        e.gc.setAlpha(200);

                        // Draw the little triangle
                        int x = mSelectedNode.left + DrawableViewNode.NODE_WIDTH / 2;
                        int y = (int) mSelectedNode.top + 4;
                        e.gc.setBackground(mBoxColor);
                        e.gc.fillPolygon(new int[] {
                                x, y, x - 11, y - 11, x + 11, y - 11
                        });

                        // Draw the rectangle and update the location.
                        y -= 10 + RECT_HEIGHT;
                        e.gc.fillRoundRectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT, 30,
                                30);
                        mSelectedRectangleLocation =
                                new Rectangle(x - RECT_WIDTH / 2, y, RECT_WIDTH, RECT_HEIGHT);

                        e.gc.setAlpha(255);

                        // Draw the button
                        mButtonCenter =
                                new Point(x - BUTTON_RIGHT_OFFSET + (RECT_WIDTH - BUTTON_SIZE) / 2,
                                        y + BUTTON_TOP_OFFSET + BUTTON_SIZE / 2);

                        if (mButtonClicked) {
                            e.gc
                                    .setBackground(Display.getDefault().getSystemColor(
                                            SWT.COLOR_BLACK));
                        } else {
                            e.gc.setBackground(mTextBackgroundColor);

                        }
                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));

                        e.gc.fillOval(x + RECT_WIDTH / 2 - BUTTON_RIGHT_OFFSET - BUTTON_SIZE, y
                                + BUTTON_TOP_OFFSET, BUTTON_SIZE, BUTTON_SIZE);

                        e.gc.drawRectangle(x - BUTTON_RIGHT_OFFSET
                                + (RECT_WIDTH - BUTTON_SIZE - RECTANGLE_SIZE) / 2 - 1, y
                                + BUTTON_TOP_OFFSET + (BUTTON_SIZE - RECTANGLE_SIZE) / 2,
                                RECTANGLE_SIZE + 1, RECTANGLE_SIZE);

                        y += 15;

                        // If there is an image, draw it.
                        if (mSelectedNode.viewNode.image != null
                                && mSelectedNode.viewNode.image.getBounds().height != 1
                                && mSelectedNode.viewNode.image.getBounds().width != 1) {

                            // Scaling the image to the right size takes lots of
                            // time, so we want to do it only once.

                            // If the selection changed, get rid of the old
                            // image.
                            if (mLastDrawnSelectedViewNode != mSelectedNode) {
                                if (mScaledSelectedImage != null) {
                                    mScaledSelectedImage.dispose();
                                    mScaledSelectedImage = null;
                                }
                                mLastDrawnSelectedViewNode = mSelectedNode;
                            }

                            if (mScaledSelectedImage == null) {
                                double ratio =
                                        1.0 * mSelectedNode.viewNode.image.getBounds().width
                                                / mSelectedNode.viewNode.image.getBounds().height;
                                int newWidth, newHeight;
                                if (ratio > 1.0 * IMAGE_WIDTH / IMAGE_HEIGHT) {
                                    newWidth =
                                            Math.min(IMAGE_WIDTH, mSelectedNode.viewNode.image
                                                    .getBounds().width);
                                    newHeight = (int) (newWidth / ratio);
                                } else {
                                    newHeight =
                                            Math.min(IMAGE_HEIGHT, mSelectedNode.viewNode.image
                                                    .getBounds().height);
                                    newWidth = (int) (newHeight * ratio);
                                }

                                // Interesting note... We make the image twice
                                // the needed size so that there is better
                                // resolution under zoom.
                                newWidth = Math.max(newWidth * 2, 1);
                                newHeight = Math.max(newHeight * 2, 1);
                                mScaledSelectedImage =
                                        new Image(Display.getDefault(), newWidth, newHeight);
                                GC gc = new GC(mScaledSelectedImage);
                                gc.setBackground(mTextBackgroundColor);
                                gc.fillRectangle(0, 0, newWidth, newHeight);
                                gc.drawImage(mSelectedNode.viewNode.image, 0, 0,
                                        mSelectedNode.viewNode.image.getBounds().width,
                                        mSelectedNode.viewNode.image.getBounds().height, 0, 0,
                                        newWidth, newHeight);
                                gc.dispose();
                            }

                            // Draw the background rectangle
                            e.gc.setBackground(mTextBackgroundColor);
                            e.gc.fillRoundRectangle(x - mScaledSelectedImage.getBounds().width / 4
                                    - IMAGE_OFFSET, y
                                    + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2)
                                    / 2 - IMAGE_OFFSET, mScaledSelectedImage.getBounds().width / 2
                                    + 2 * IMAGE_OFFSET, mScaledSelectedImage.getBounds().height / 2
                                    + 2 * IMAGE_OFFSET, IMAGE_ROUNDING, IMAGE_ROUNDING);

                            // Under max zoom, we want the image to be
                            // untransformed. So, get back to the identity
                            // transform.
                            int imageX = x - mScaledSelectedImage.getBounds().width / 4;
                            int imageY =
                                    y
                                            + (IMAGE_HEIGHT - mScaledSelectedImage.getBounds().height / 2)
                                            / 2;

                            Transform untransformedTransform = new Transform(Display.getDefault());
                            e.gc.setTransform(untransformedTransform);
                            float[] pt = new float[] {
                                    imageX, imageY
                            };
                            mTransform.transform(pt);
                            e.gc.drawImage(mScaledSelectedImage, 0, 0, mScaledSelectedImage
                                    .getBounds().width, mScaledSelectedImage.getBounds().height,
                                    (int) pt[0], (int) pt[1], (int) (mScaledSelectedImage
                                            .getBounds().width
                                            * mZoom / 2),
                                    (int) (mScaledSelectedImage.getBounds().height * mZoom / 2));
                            untransformedTransform.dispose();
                            e.gc.setTransform(mTransform);
                        }

                        // Text stuff

                        y += IMAGE_HEIGHT;
                        y += 10;
                        Font font = getFont(8, false);
                        e.gc.setFont(font);

                        String text =
                                mSelectedNode.viewNode.viewCount + " view"
                                        + (mSelectedNode.viewNode.viewCount != 1 ? "s" : "");
                        DecimalFormat formatter = new DecimalFormat("0.000");

                        String measureText =
                                "Measure: "
                                        + (mSelectedNode.viewNode.measureTime != -1 ? formatter
                                                .format(mSelectedNode.viewNode.measureTime)
                                                + " ms" : "n/a");
                        String layoutText =
                                "Layout: "
                                        + (mSelectedNode.viewNode.layoutTime != -1 ? formatter
                                                .format(mSelectedNode.viewNode.layoutTime)
                                                + " ms" : "n/a");
                        String drawText =
                                "Draw: "
                                        + (mSelectedNode.viewNode.drawTime != -1 ? formatter
                                                .format(mSelectedNode.viewNode.drawTime)
                                                + " ms" : "n/a");

                        org.eclipse.swt.graphics.Point titleExtent = e.gc.stringExtent(text);
                        org.eclipse.swt.graphics.Point measureExtent =
                                e.gc.stringExtent(measureText);
                        org.eclipse.swt.graphics.Point layoutExtent = e.gc.stringExtent(layoutText);
                        org.eclipse.swt.graphics.Point drawExtent = e.gc.stringExtent(drawText);
                        int boxWidth =
                                Math.max(titleExtent.x, Math.max(measureExtent.x, Math.max(
                                        layoutExtent.x, drawExtent.x)))
                                        + 2 * TEXT_SIDE_OFFSET;
                        int boxHeight =
                                titleExtent.y + TEXT_SPACING + measureExtent.y + TEXT_SPACING
                                        + layoutExtent.y + TEXT_SPACING + drawExtent.y + 2
                                        * TEXT_TOP_OFFSET;

                        e.gc.setBackground(mTextBackgroundColor);
                        e.gc.fillRoundRectangle(x - boxWidth / 2, y, boxWidth, boxHeight,
                                TEXT_ROUNDING, TEXT_ROUNDING);

                        e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));

                        y += TEXT_TOP_OFFSET;

                        e.gc.drawText(text, x - titleExtent.x / 2, y, true);

                        x -= boxWidth / 2;
                        x += TEXT_SIDE_OFFSET;

                        y += titleExtent.y + TEXT_SPACING;

                        e.gc.drawText(measureText, x, y, true);

                        y += measureExtent.y + TEXT_SPACING;

                        e.gc.drawText(layoutText, x, y, true);

                        y += layoutExtent.y + TEXT_SPACING;

                        e.gc.drawText(drawText, x, y, true);

                        font.dispose();
                    } else {
                        mSelectedRectangleLocation = null;
                        mButtonCenter = null;
                    }
                }
            }
        }
    };

    private static void paintRecursive(GC gc, Transform transform, DrawableViewNode node,
            DrawableViewNode selectedNode, Path connectionPath) {
        if (selectedNode == node && node.viewNode.filtered) {
            gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top));
        } else if (selectedNode == node) {
            gc.drawImage(sSelectedImage, node.left, (int) Math.round(node.top));
        } else if (node.viewNode.filtered) {
            gc.drawImage(sFilteredImage, node.left, (int) Math.round(node.top));
        } else {
            gc.drawImage(sNotSelectedImage, node.left, (int) Math.round(node.top));
        }

        int fontHeight = gc.getFontMetrics().getHeight();

        // Draw the text...
        int contentWidth =
                DrawableViewNode.NODE_WIDTH - 2 * DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
        String name = node.viewNode.name;
        int dotIndex = name.lastIndexOf('.');
        if (dotIndex != -1) {
            name = name.substring(dotIndex + 1);
        }
        double x = node.left + DrawableViewNode.CONTENT_LEFT_RIGHT_PADDING;
        double y = node.top + DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING;
        drawTextInArea(gc, transform, name, x, y, contentWidth, fontHeight, 10, true);

        y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;

        drawTextInArea(gc, transform, "@" + node.viewNode.hashCode, x, y, contentWidth, fontHeight,
                8, false);

        y += fontHeight + DrawableViewNode.CONTENT_INTER_PADDING;
        if (!node.viewNode.id.equals("NO_ID")) {
            drawTextInArea(gc, transform, node.viewNode.id, x, y, contentWidth, fontHeight, 8,
                    false);
        }

        if (node.viewNode.measureRating != ProfileRating.NONE) {
            y =
                    node.top + DrawableViewNode.NODE_HEIGHT
                            - DrawableViewNode.CONTENT_TOP_BOTTOM_PADDING
                            - sRedImage.getBounds().height;
            x +=
                    (contentWidth - (sRedImage.getBounds().width * 3 + 2 * DrawableViewNode.CONTENT_INTER_PADDING)) / 2;
            switch (node.viewNode.measureRating) {
                case GREEN:
                    gc.drawImage(sGreenImage, (int) x, (int) y);
                    break;
                case YELLOW:
                    gc.drawImage(sYellowImage, (int) x, (int) y);
                    break;
                case RED:
                    gc.drawImage(sRedImage, (int) x, (int) y);
                    break;
            }

            x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
            switch (node.viewNode.layoutRating) {
                case GREEN:
                    gc.drawImage(sGreenImage, (int) x, (int) y);
                    break;
                case YELLOW:
                    gc.drawImage(sYellowImage, (int) x, (int) y);
                    break;
                case RED:
                    gc.drawImage(sRedImage, (int) x, (int) y);
                    break;
            }

            x += sRedImage.getBounds().width + DrawableViewNode.CONTENT_INTER_PADDING;
            switch (node.viewNode.drawRating) {
                case GREEN:
                    gc.drawImage(sGreenImage, (int) x, (int) y);
                    break;
                case YELLOW:
                    gc.drawImage(sYellowImage, (int) x, (int) y);
                    break;
                case RED:
                    gc.drawImage(sRedImage, (int) x, (int) y);
                    break;
            }
        }

        org.eclipse.swt.graphics.Point indexExtent =
                gc.stringExtent(Integer.toString(node.viewNode.index));
        x =
                node.left + DrawableViewNode.NODE_WIDTH - DrawableViewNode.INDEX_PADDING
                        - indexExtent.x;
        y =
                node.top + DrawableViewNode.NODE_HEIGHT - DrawableViewNode.INDEX_PADDING
                        - indexExtent.y;
        gc.drawText(Integer.toString(node.viewNode.index), (int) x, (int) y, SWT.DRAW_TRANSPARENT);

        int N = node.children.size();
        if (N == 0) {
            return;
        }
        float childSpacing = (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * LINE_PADDING)) / N;
        for (int i = 0; i < N; i++) {
            DrawableViewNode child = node.children.get(i);
            paintRecursive(gc, transform, child, selectedNode, connectionPath);
            float x1 = node.left + DrawableViewNode.NODE_WIDTH;
            float y1 = (float) node.top + LINE_PADDING + childSpacing * i + childSpacing / 2;
            float x2 = child.left;
            float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f;
            float cx1 = x1 + BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
            float cy1 = y1;
            float cx2 = x2 - BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
            float cy2 = y2;
            connectionPath.moveTo(x1, y1);
            connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2);
        }
    }

    private static void drawTextInArea(GC gc, Transform transform, String text, double x, double y,
            double width, double height, int fontSize, boolean bold) {

        Font oldFont = gc.getFont();

        Font newFont = getFont(fontSize, bold);
        gc.setFont(newFont);

        org.eclipse.swt.graphics.Point extent = gc.stringExtent(text);

        if (extent.x > width) {
            // Oh no... we need to scale it.
            double scale = width / extent.x;
            float[] transformElements = new float[6];
            transform.getElements(transformElements);
            transform.scale((float) scale, (float) scale);
            gc.setTransform(transform);

            x /= scale;
            y /= scale;
            y += (extent.y / scale - extent.y) / 2;

            gc.drawText(text, (int) x, (int) y, SWT.DRAW_TRANSPARENT);

            transform.setElements(transformElements[0], transformElements[1], transformElements[2],
                    transformElements[3], transformElements[4], transformElements[5]);
            gc.setTransform(transform);
        } else {
            gc.drawText(text, (int) (x + (width - extent.x) / 2),
                    (int) (y + (height - extent.y) / 2), SWT.DRAW_TRANSPARENT);
        }
        gc.setFont(oldFont);
        newFont.dispose();

    }

    public static Image paintToImage(DrawableViewNode tree) {
        Image image =
                new Image(Display.getDefault(), (int) Math.ceil(tree.bounds.width), (int) Math
                        .ceil(tree.bounds.height));

        Transform transform = new Transform(Display.getDefault());
        transform.identity();
        transform.translate((float) -tree.bounds.x, (float) -tree.bounds.y);
        Path connectionPath = new Path(Display.getDefault());
        GC gc = new GC(image);

        // Can't use Display.getDefault().getSystemColor in a non-UI thread.
        Color white = new Color(Display.getDefault(), 255, 255, 255);
        Color black = new Color(Display.getDefault(), 0, 0, 0);
        gc.setForeground(white);
        gc.setBackground(black);
        gc.fillRectangle(0, 0, image.getBounds().width, image.getBounds().height);
        gc.setTransform(transform);
        paintRecursive(gc, transform, tree, null, connectionPath);
        gc.drawPath(connectionPath);
        gc.dispose();
        connectionPath.dispose();
        white.dispose();
        black.dispose();
        return image;
    }

    private static Font getFont(int size, boolean bold) {
        FontData[] fontData = sSystemFont.getFontData();
        for (int i = 0; i < fontData.length; i++) {
            fontData[i].setHeight(size);
            if (bold) {
                fontData[i].setStyle(SWT.BOLD);
            }
        }
        return new Font(Display.getDefault(), fontData);
    }

    private void doRedraw() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                redraw();
            }
        });
    }

    public void loadAllData() {
        boolean newViewport = mViewport == null;
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    mTree = mModel.getTree();
                    mSelectedNode = mModel.getSelection();
                    mViewport = mModel.getViewport();
                    mZoom = mModel.getZoom();
                    if (mTree != null && mViewport == null) {
                        mViewport =
                                new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2
                                        - getBounds().height / 2, getBounds().width,
                                        getBounds().height);
                    } else {
                        setTransform();
                    }
                }
            }
        });
        if (newViewport) {
            mModel.setViewport(mViewport);
        }
    }

    // Fickle behaviour... When a new tree is loaded, the model doesn't know
    // about the viewport until it passes through here.
    @Override
    public void treeChanged() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    mTree = mModel.getTree();
                    mSelectedNode = mModel.getSelection();
                    if (mTree == null) {
                        mViewport = null;
                    } else {
                        mViewport =
                                new Rectangle(0, mTree.top + DrawableViewNode.NODE_HEIGHT / 2
                                        - getBounds().height / 2, getBounds().width,
                                        getBounds().height);
                    }
                }
            }
        });
        if (mViewport != null) {
            mModel.setViewport(mViewport);
        } else {
            doRedraw();
        }
    }

    private void setTransform() {
        if (mViewport != null && mTree != null) {
            // Set the transform.
            mTransform.identity();
            mInverse.identity();

            mTransform.scale((float) mZoom, (float) mZoom);
            mInverse.scale((float) mZoom, (float) mZoom);
            mTransform.translate((float) -mViewport.x, (float) -mViewport.y);
            mInverse.translate((float) -mViewport.x, (float) -mViewport.y);
            mInverse.invert();
        }
    }

    // Note the syncExec and then synchronized... It avoids deadlock
    @Override
    public void viewportChanged() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    mViewport = mModel.getViewport();
                    mZoom = mModel.getZoom();
                    setTransform();
                }
            }
        });
        doRedraw();
    }

    @Override
    public void zoomChanged() {
        viewportChanged();
    }

    @Override
    public void selectionChanged() {
        synchronized (this) {
            mSelectedNode = mModel.getSelection();
            if (mSelectedNode != null && mSelectedNode.viewNode.image == null) {
                HierarchyViewerDirector.getDirector()
                        .loadCaptureInBackground(mSelectedNode.viewNode);
            }
        }
        doRedraw();
    }
}
