/*
 * 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.models.TreeViewModel;
import com.android.hierarchyviewerlib.models.TreeViewModel.ITreeChangeListener;
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.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Path;
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;

public class TreeViewOverview extends Canvas implements ITreeChangeListener {

    private TreeViewModel mModel;

    private DrawableViewNode mTree;

    private Rectangle mViewport;

    private Transform mTransform;

    private Transform mInverse;

    private Rectangle mBounds = new Rectangle();

    private double mScale;

    private boolean mDragging = false;

    private DrawableViewNode mSelectedNode;

    private static Image sNotSelectedImage;

    private static Image sSelectedImage;

    private static Image sFilteredImage;

    private static Image sFilteredSelectedImage;

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

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

        loadResources();

        addPaintListener(mPaintListener);
        addMouseListener(mMouseListener);
        addMouseMoveListener(mMouseMoveListener);
        addListener(SWT.Resize, mResizeListener);
        addDisposeListener(mDisposeListener);

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

        loadAllData();
    }

    private void loadResources() {
        ImageLoader loader = ImageLoader.getLoader(this.getClass());
        sNotSelectedImage = loader.loadImage("not-selected.png", Display.getDefault()); //$NON-NLS-1$
        sSelectedImage = loader.loadImage("selected-small.png", Display.getDefault()); //$NON-NLS-1$
        sFilteredImage = loader.loadImage("filtered.png", Display.getDefault()); //$NON-NLS-1$
        sFilteredSelectedImage =
                loader.loadImage("selected-filtered-small.png", Display.getDefault()); //$NON-NLS-1$
    }

    private DisposeListener mDisposeListener = new DisposeListener() {
        @Override
        public void widgetDisposed(DisposeEvent e) {
            mModel.removeTreeChangeListener(TreeViewOverview.this);
            mTransform.dispose();
            mInverse.dispose();
        }
    };

    private MouseListener mMouseListener = new MouseListener() {

        @Override
        public void mouseDoubleClick(MouseEvent e) {
            // pass
        }

        @Override
        public void mouseDown(MouseEvent e) {
            boolean redraw = false;
            synchronized (TreeViewOverview.this) {
                if (mTree != null && mViewport != null) {
                    mDragging = true;
                    redraw = true;
                    handleMouseEvent(transformPoint(e.x, e.y));
                }
            }
            if (redraw) {
                mModel.removeTreeChangeListener(TreeViewOverview.this);
                mModel.setViewport(mViewport);
                mModel.addTreeChangeListener(TreeViewOverview.this);
                doRedraw();
            }
        }

        @Override
        public void mouseUp(MouseEvent e) {
            boolean redraw = false;
            synchronized (TreeViewOverview.this) {
                if (mTree != null && mViewport != null) {
                    mDragging = false;
                    redraw = true;
                    handleMouseEvent(transformPoint(e.x, e.y));

                    // Update bounds and transform only on mouse up. That way,
                    // you don't get confusing behaviour during mouse drag and
                    // it snaps neatly at the end
                    setBounds();
                    setTransform();
                }
            }
            if (redraw) {
                mModel.removeTreeChangeListener(TreeViewOverview.this);
                mModel.setViewport(mViewport);
                mModel.addTreeChangeListener(TreeViewOverview.this);
                doRedraw();
            }
        }

    };

    private MouseMoveListener mMouseMoveListener = new MouseMoveListener() {
        @Override
        public void mouseMove(MouseEvent e) {
            boolean moved = false;
            synchronized (TreeViewOverview.this) {
                if (mDragging) {
                    moved = true;
                    handleMouseEvent(transformPoint(e.x, e.y));
                }
            }
            if (moved) {
                mModel.removeTreeChangeListener(TreeViewOverview.this);
                mModel.setViewport(mViewport);
                mModel.addTreeChangeListener(TreeViewOverview.this);
                doRedraw();
            }
        }
    };

    private void handleMouseEvent(Point pt) {
        mViewport.x = pt.x - mViewport.width / 2;
        mViewport.y = pt.y - mViewport.height / 2;
        if (mViewport.x < mBounds.x) {
            mViewport.x = mBounds.x;
        }
        if (mViewport.y < mBounds.y) {
            mViewport.y = mBounds.y;
        }
        if (mViewport.x + mViewport.width > mBounds.x + mBounds.width) {
            mViewport.x = mBounds.x + mBounds.width - mViewport.width;
        }
        if (mViewport.y + mViewport.height > mBounds.y + mBounds.height) {
            mViewport.y = mBounds.y + mBounds.height - mViewport.height;
        }
    }

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

    private Listener mResizeListener = new Listener() {
        @Override
        public void handleEvent(Event arg0) {
            synchronized (TreeViewOverview.this) {
                setTransform();
            }
            doRedraw();
        }
    };

    private PaintListener mPaintListener = new PaintListener() {
        @Override
        public void paintControl(PaintEvent e) {
            synchronized (TreeViewOverview.this) {
                if (mTree != null) {
                    e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
                    e.gc.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
                    e.gc.fillRectangle(0, 0, getBounds().width, getBounds().height);
                    e.gc.setTransform(mTransform);
                    e.gc.setLineWidth((int) Math.ceil(0.7 / mScale));
                    Path connectionPath = new Path(Display.getDefault());
                    paintRecursive(e.gc, mTree, connectionPath);
                    e.gc.drawPath(connectionPath);
                    connectionPath.dispose();

                    if (mViewport != null) {
                        e.gc.setAlpha(50);
                        e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
                        e.gc.fillRectangle((int) mViewport.x, (int) mViewport.y, (int) Math
                                .ceil(mViewport.width), (int) Math.ceil(mViewport.height));

                        e.gc.setAlpha(255);
                        e.gc.setForeground(Display.getDefault().getSystemColor(
                                        SWT.COLOR_DARK_GRAY));
                        e.gc.setLineWidth((int) Math.ceil(2 / mScale));
                        e.gc.drawRectangle((int) mViewport.x, (int) mViewport.y, (int) Math
                                .ceil(mViewport.width), (int) Math.ceil(mViewport.height));
                    }
                }
            }
        }
    };

    private void paintRecursive(GC gc, DrawableViewNode node, Path connectionPath) {
        if (mSelectedNode == node && node.viewNode.filtered) {
            gc.drawImage(sFilteredSelectedImage, node.left, (int) Math.round(node.top));
        } else if (mSelectedNode == 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 N = node.children.size();
        if (N == 0) {
            return;
        }
        float childSpacing =
                (1.0f * (DrawableViewNode.NODE_HEIGHT - 2 * TreeView.LINE_PADDING)) / N;
        for (int i = 0; i < N; i++) {
            DrawableViewNode child = node.children.get(i);
            paintRecursive(gc, child, connectionPath);
            float x1 = node.left + DrawableViewNode.NODE_WIDTH;
            float y1 =
                    (float) node.top + TreeView.LINE_PADDING + childSpacing * i + childSpacing / 2;
            float x2 = child.left;
            float y2 = (float) child.top + DrawableViewNode.NODE_HEIGHT / 2.0f;
            float cx1 = x1 + TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
            float cy1 = y1;
            float cx2 = x2 - TreeView.BEZIER_FRACTION * DrawableViewNode.PARENT_CHILD_SPACING;
            float cy2 = y2;
            connectionPath.moveTo(x1, y1);
            connectionPath.cubicTo(cx1, cy1, cx2, cy2, x2, y2);
        }
    }

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

    public void loadAllData() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    mTree = mModel.getTree();
                    mSelectedNode = mModel.getSelection();
                    mViewport = mModel.getViewport();
                    setBounds();
                    setTransform();
                }
            }
        });
    }

    // Note the syncExec and then synchronized... It avoids deadlock
    @Override
    public void treeChanged() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    mTree = mModel.getTree();
                    mSelectedNode = mModel.getSelection();
                    mViewport = mModel.getViewport();
                    setBounds();
                    setTransform();
                }
            }
        });
        doRedraw();
    }

    private void setBounds() {
        if (mViewport != null && mTree != null) {
            mBounds.x = Math.min(mViewport.x, mTree.bounds.x);
            mBounds.y = Math.min(mViewport.y, mTree.bounds.y);
            mBounds.width =
                    Math.max(mViewport.x + mViewport.width, mTree.bounds.x + mTree.bounds.width)
                            - mBounds.x;
            mBounds.height =
                    Math.max(mViewport.y + mViewport.height, mTree.bounds.y + mTree.bounds.height)
                            - mBounds.y;
        } else if (mTree != null) {
            mBounds.x = mTree.bounds.x;
            mBounds.y = mTree.bounds.y;
            mBounds.width = mTree.bounds.x + mTree.bounds.width - mBounds.x;
            mBounds.height = mTree.bounds.y + mTree.bounds.height - mBounds.y;
        }
    }

    private void setTransform() {
        if (mTree != null) {

            mTransform.identity();
            mInverse.identity();
            final Point size = new Point();
            size.x = getBounds().width;
            size.y = getBounds().height;
            if (mBounds.width == 0 || mBounds.height == 0 || size.x == 0 || size.y == 0) {
                mScale = 1;
            } else {
                mScale = Math.min(size.x / mBounds.width, size.y / mBounds.height);
            }
            mTransform.scale((float) mScale, (float) mScale);
            mInverse.scale((float) mScale, (float) mScale);
            mTransform.translate((float) -mBounds.x, (float) -mBounds.y);
            mInverse.translate((float) -mBounds.x, (float) -mBounds.y);
            if (size.x / mBounds.width < size.y / mBounds.height) {
                mTransform.translate(0, (float) (size.y / mScale - mBounds.height) / 2);
                mInverse.translate(0, (float) (size.y / mScale - mBounds.height) / 2);
            } else {
                mTransform.translate((float) (size.x / mScale - mBounds.width) / 2, 0);
                mInverse.translate((float) (size.x / mScale - mBounds.width) / 2, 0);
            }
            mInverse.invert();
        }
    }

    @Override
    public void viewportChanged() {
        Display.getDefault().syncExec(new Runnable() {
            @Override
            public void run() {
                synchronized (this) {
                    mViewport = mModel.getViewport();
                    setBounds();
                    setTransform();
                }
            }
        });
        doRedraw();
    }

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

    @Override
    public void selectionChanged() {
        synchronized (this) {
            mSelectedNode = mModel.getSelection();
        }
        doRedraw();
    }
}
