/*
 * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 *
 */
package com.sun.hotspot.igv.view;

import com.sun.hotspot.igv.view.widgets.BlockWidget;
import com.sun.hotspot.igv.view.widgets.LineWidget;
import com.sun.hotspot.igv.util.DoubleClickAction;
import com.sun.hotspot.igv.data.InputBlock;
import com.sun.hotspot.igv.data.InputNode;
import com.sun.hotspot.igv.graph.Connection;
import com.sun.hotspot.igv.graph.Diagram;
import com.sun.hotspot.igv.graph.Figure;
import com.sun.hotspot.igv.graph.InputSlot;
import com.sun.hotspot.igv.graph.OutputSlot;
import com.sun.hotspot.igv.graph.Slot;
import com.sun.hotspot.igv.hierarchicallayout.HierarchicalClusterLayoutManager;
import com.sun.hotspot.igv.hierarchicallayout.OldHierarchicalLayoutManager;
import com.sun.hotspot.igv.hierarchicallayout.HierarchicalLayoutManager;
import com.sun.hotspot.igv.view.widgets.FigureWidget;
import com.sun.hotspot.igv.view.widgets.InputSlotWidget;
import com.sun.hotspot.igv.view.widgets.OutputSlotWidget;
import com.sun.hotspot.igv.view.widgets.SlotWidget;
import com.sun.hotspot.igv.layout.LayoutGraph;
import com.sun.hotspot.igv.data.services.Scheduler;
import com.sun.hotspot.igv.data.ChangedListener;
import com.sun.hotspot.igv.graph.Block;
import com.sun.hotspot.igv.util.ColorIcon;
import com.sun.hotspot.igv.util.ExtendedSelectAction;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.UndoableEditEvent;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import org.netbeans.api.visual.action.ActionFactory;
import org.netbeans.api.visual.action.PopupMenuProvider;
import org.netbeans.api.visual.action.RectangularSelectDecorator;
import org.netbeans.api.visual.action.RectangularSelectProvider;
import org.netbeans.api.visual.action.SelectProvider;
import org.netbeans.api.visual.action.WidgetAction;
import org.netbeans.api.visual.animator.SceneAnimator;
import org.netbeans.api.visual.layout.LayoutFactory;
import org.netbeans.api.visual.widget.ConnectionWidget;
import org.netbeans.api.visual.widget.LayerWidget;
import org.netbeans.api.visual.widget.Scene;
import org.netbeans.api.visual.widget.Widget;
import org.netbeans.api.visual.widget.LabelWidget;
import org.openide.awt.UndoRedo;
import org.openide.util.Lookup;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;

/**
 *
 * @author Thomas Wuerthinger
 */
public class DiagramScene extends Scene implements ChangedListener<DiagramViewModel> {

    private HashMap<Figure, FigureWidget> figureWidgets;
    private HashMap<Slot, SlotWidget> slotWidgets;
    private HashMap<Connection, ConnectionWidget> connectionWidgets;
    private HashMap<InputBlock, BlockWidget> blockWidgets;
    private Widget hoverWidget;
    private WidgetAction hoverAction;
    private List<FigureWidget> selectedWidgets;
    private Lookup lookup;
    private InstanceContent content;
    private Action[] actions;
    private LayerWidget connectionLayer;
    private JScrollPane scrollPane;
    private UndoRedo.Manager undoRedoManager;
    private LayerWidget mainLayer;
    private LayerWidget slotLayer;
    private LayerWidget blockLayer;
    private double realZoomFactor;
    private BoundedZoomAction zoomAction;
    private WidgetAction panAction;
    private Widget topLeft;
    private Widget bottomRight;
    private LayerWidget startLayer;
    private LabelWidget startLabel;
    private DiagramViewModel model;
    private DiagramViewModel modelCopy;
    public static final int AFTER = 1;
    public static final int BEFORE = 1;
    public static final float ALPHA = 0.4f;
    public static final int GRID_SIZE = 30;
    public static final int BORDER_SIZE = 20;
    public static final int UNDOREDO_LIMIT = 100;
    public static final int SCROLL_UNIT_INCREMENT = 80;
    public static final int SCROLL_BLOCK_INCREMENT = 400;
    public static final float ZOOM_MAX_FACTOR = 3.0f;
    public static final float ZOOM_MIN_FACTOR = 0.0f;//0.15f;
    public static final float ZOOM_INCREMENT = 1.5f;
    public static final int SLOT_OFFSET = 6;
    public static final int ANIMATION_LIMIT = 40;
    private PopupMenuProvider popupMenuProvider = new PopupMenuProvider() {

        public JPopupMenu getPopupMenu(Widget widget, Point localLocation) {
            return DiagramScene.this.createPopupMenu();
        }
    };
    private RectangularSelectDecorator rectangularSelectDecorator = new RectangularSelectDecorator() {

        public Widget createSelectionWidget() {
            Widget widget = new Widget(DiagramScene.this);
            widget.setBorder(BorderFactory.createLineBorder(Color.black, 2));
            widget.setForeground(Color.red);
            return widget;
        }
    };
    private RectangularSelectProvider rectangularSelectProvider = new RectangularSelectProvider() {

        public void performSelection(Rectangle rectangle) {
            if (rectangle.width < 0) {
                rectangle.x += rectangle.width;
                rectangle.width *= -1;
            }

            if (rectangle.height < 0) {
                rectangle.y += rectangle.height;
                rectangle.height *= -1;
            }

            boolean updated = false;
            for (Figure f : getModel().getDiagramToView().getFigures()) {
                FigureWidget w = figureWidgets.get(f);
                Rectangle r = new Rectangle(w.getBounds());
                r.setLocation(w.getLocation());
                if (r.intersects(rectangle)) {
                    if (!selectedWidgets.contains(w)) {
                        addToSelection(w);
                        updated = true;
                    }
                } else {
                    if (selectedWidgets.contains(w)) {
                        selectedWidgets.remove(w);
                        content.remove(w.getNode());
                        w.setState(w.getState().deriveSelected(false));
                        updated = true;
                    }
                }
            }

            if (updated) {
                selectionUpdated();
            }
        }
    };
    private SelectProvider selectProvider = new SelectProvider() {

        public boolean isAimingAllowed(Widget widget, Point point, boolean b) {
            return false;
        }

        public boolean isSelectionAllowed(Widget widget, Point point, boolean b) {
            return widget instanceof FigureWidget || widget == DiagramScene.this;
        }

        public void select(Widget w, Point point, boolean change) {

            boolean updated = false;

            if (w == DiagramScene.this) {
                if (DiagramScene.this.selectedWidgets.size() != 0) {
                    clearSelection();
                    selectionUpdated();
                }
                return;
            }

            FigureWidget widget = (FigureWidget) w;


            if (change) {
                if (widget.getState().isSelected()) {
                    assert selectedWidgets.contains(widget);
                    widget.setState(widget.getState().deriveSelected(false));
                    selectedWidgets.remove(widget);
                    content.remove(widget.getNode());
                    updated = true;
                } else {
                    assert !selectedWidgets.contains(widget);
                    addToSelection(widget);
                    updated = true;
                    assert widget.getState().isSelected();
                }
            } else {

                if (widget.getState().isSelected()) {
                    assert selectedWidgets.contains(widget);
                } else {

                    assert !selectedWidgets.contains(widget);
                    clearSelection();
                    addToSelection(widget);
                    updated = true;
                    assert widget.getState().isSelected();
                }
            }

            if (updated) {
                selectionUpdated();
            }

        }
    };

    private FigureWidget getFigureWidget(Figure f) {
        return figureWidgets.get(f);
    }
    private FocusListener focusListener = new FocusListener() {

        public void focusGained(FocusEvent e) {
            DiagramScene.this.getView().requestFocus();
        }

        public void focusLost(FocusEvent e) {
        }
    };
    private MouseWheelListener mouseWheelListener = new MouseWheelListener() {

        public void mouseWheelMoved(MouseWheelEvent e) {
            DiagramScene.this.zoomAction.mouseWheelMoved(DiagramScene.this, new WidgetAction.WidgetMouseWheelEvent(0, e));
            DiagramScene.this.validate();
        }
    };
    private MouseListener mouseListener = new MouseListener() {

        public void mouseClicked(MouseEvent e) {
            DiagramScene.this.panAction.mouseClicked(DiagramScene.this, new WidgetAction.WidgetMouseEvent(0, e));
        }

        public void mousePressed(MouseEvent e) {
            DiagramScene.this.panAction.mousePressed(DiagramScene.this, new WidgetAction.WidgetMouseEvent(0, e));
        }

        public void mouseReleased(MouseEvent e) {
            DiagramScene.this.panAction.mouseReleased(DiagramScene.this, new WidgetAction.WidgetMouseEvent(0, e));
        }

        public void mouseEntered(MouseEvent e) {
            DiagramScene.this.panAction.mouseEntered(DiagramScene.this, new WidgetAction.WidgetMouseEvent(0, e));
        }

        public void mouseExited(MouseEvent e) {
            DiagramScene.this.panAction.mouseExited(DiagramScene.this, new WidgetAction.WidgetMouseEvent(0, e));
        }
    };
    private MouseMotionListener mouseMotionListener = new MouseMotionListener() {

        public void mouseDragged(MouseEvent e) {
            DiagramScene.this.panAction.mouseDragged(DiagramScene.this, new WidgetAction.WidgetMouseEvent(0, e));
        }

        public void mouseMoved(MouseEvent e) {
        }
    };
    private ScrollChangeListener scrollChangeListener = new ScrollChangeListener();

    private class ScrollChangeListener implements ChangeListener {

        private Map<Widget, Point> relativePositions = new HashMap<Widget, Point>();
        private Point oldPosition;

        public void register(Widget w, Point p) {
            relativePositions.put(w, p);
        }

        public void unregister(Widget w) {
            relativePositions.remove(w);
        }

        public void stateChanged(ChangeEvent e) {
            Point p = DiagramScene.this.getScrollPane().getViewport().getViewPosition();
            if (oldPosition == null || !p.equals(oldPosition)) {
                for (Widget w : relativePositions.keySet()) {
                    Point curPoint = relativePositions.get(w);
                    Point newPoint = new Point(p.x + curPoint.x, p.y + curPoint.y);
                    w.setPreferredLocation(newPoint);
                    DiagramScene.this.validate();
                }
                oldPosition = p;
            }
        }
    }

    public Point getScrollPosition() {
        return getScrollPane().getViewport().getViewPosition();
    }

    public void setScrollPosition(Point p) {
        getScrollPane().getViewport().setViewPosition(p);
    }

    public DiagramScene(Action[] actions, DiagramViewModel model) {
        this.actions = actions;
        selectedWidgets = new ArrayList<FigureWidget>();
        content = new InstanceContent();
        lookup = new AbstractLookup(content);
        this.setCheckClipping(true);
        this.getInputBindings().setZoomActionModifiers(0);

        JComponent comp = this.createView();
        comp.setDoubleBuffered(true);
        comp.setBackground(Color.WHITE);
        comp.setOpaque(true);

        this.setBackground(Color.WHITE);
        this.setOpaque(true);
        scrollPane = new JScrollPane(comp);
        scrollPane.setBackground(Color.WHITE);
        scrollPane.getVerticalScrollBar().setUnitIncrement(SCROLL_UNIT_INCREMENT);
        scrollPane.getVerticalScrollBar().setBlockIncrement(SCROLL_BLOCK_INCREMENT);
        scrollPane.getHorizontalScrollBar().setUnitIncrement(SCROLL_UNIT_INCREMENT);
        scrollPane.getHorizontalScrollBar().setBlockIncrement(SCROLL_BLOCK_INCREMENT);
        scrollPane.getViewport().addChangeListener(scrollChangeListener);
        hoverAction = this.createWidgetHoverAction();

        blockLayer = new LayerWidget(this);
        this.addChild(blockLayer);

        startLayer = new LayerWidget(this);
        this.addChild(startLayer);
        // TODO: String startLabelString = "Loading graph with " + originalDiagram.getFigures().size() + " figures and " + originalDiagram.getConnections().size() + " connections...";
        String startLabelString = "";
        LabelWidget w = new LabelWidget(this, startLabelString);
        scrollChangeListener.register(w, new Point(10, 10));
        w.setAlignment(LabelWidget.Alignment.CENTER);
        startLabel = w;
        startLayer.addChild(w);

        mainLayer = new LayerWidget(this);
        this.addChild(mainLayer);

        topLeft = new Widget(this);
        topLeft.setPreferredLocation(new Point(-BORDER_SIZE, -BORDER_SIZE));
        this.addChild(topLeft);


        bottomRight = new Widget(this);
        bottomRight.setPreferredLocation(new Point(-BORDER_SIZE, -BORDER_SIZE));
        this.addChild(bottomRight);

        slotLayer = new LayerWidget(this);
        this.addChild(slotLayer);

        connectionLayer = new LayerWidget(this);
        this.addChild(connectionLayer);

        LayerWidget selectionLayer = new LayerWidget(this);
        this.addChild(selectionLayer);

        this.setLayout(LayoutFactory.createAbsoluteLayout());

        this.getActions().addAction(hoverAction);
        zoomAction = new BoundedZoomAction(1.1, false);
        zoomAction.setMaxFactor(ZOOM_MAX_FACTOR);
        zoomAction.setMinFactor(ZOOM_MIN_FACTOR);
        this.getActions().addAction(ActionFactory.createMouseCenteredZoomAction(1.1));
        panAction = new ExtendedPanAction();
        this.getActions().addAction(panAction);
        this.getActions().addAction(ActionFactory.createPopupMenuAction(popupMenuProvider));

        LayerWidget selectLayer = new LayerWidget(this);
        this.addChild(selectLayer);
        this.getActions().addAction(ActionFactory.createRectangularSelectAction(rectangularSelectDecorator, selectLayer, rectangularSelectProvider));

        blockWidgets = new HashMap<InputBlock, BlockWidget>();

        boolean b = this.getUndoRedoEnabled();
        this.setUndoRedoEnabled(false);
        this.setNewModel(model);
        this.setUndoRedoEnabled(b);
    }

    private void selectionUpdated() {
        getModel().setSelectedNodes(this.getSelectedNodes());
        addUndo();
    }

    public DiagramViewModel getModel() {
        return model;
    }

    public void setRealZoomFactor(double d) {
        this.realZoomFactor = d;
    }

    public double getRealZoomFactor() {
        if (realZoomFactor == 0.0) {
            return getZoomFactor();
        } else {
            return realZoomFactor;
        }
    }

    public JScrollPane getScrollPane() {
        return scrollPane;
    }

    public boolean isAllVisible() {
        return getModel().getHiddenNodes().size() == 0;
    }

    public Action createGotoAction(final Figure f) {
        final DiagramScene diagramScene = this;
        Action a = new AbstractAction() {

            public void actionPerformed(ActionEvent e) {
                diagramScene.gotoFigure(f);
            }
        };

        a.setEnabled(true);
        a.putValue(Action.SMALL_ICON, new ColorIcon(f.getColor()));
        String name = f.getLines()[0];

        name += " (";

        if (f.getCluster() != null) {
            name += "B" + f.getCluster().toString();
        }
        if (!this.getFigureWidget(f).isVisible()) {
            if (f.getCluster() != null) {
                name += ", ";
            }
            name += "hidden";
        }
        name += ")";
        a.putValue(Action.NAME, name);
        return a;
    }

    public void setNewModel(DiagramViewModel model) {
        if (this.model != null) {
            this.model.getDiagramChangedEvent().removeListener(this);
            this.model.getViewPropertiesChangedEvent().removeListener(this);
        }
        this.model = model;

        if (this.model == null) {
            this.modelCopy = null;
        } else {
            this.modelCopy = this.model.copy();
        }

        model.getDiagramChangedEvent().addListener(this);
        model.getViewPropertiesChangedEvent().addListener(this);

        update();
    }

    private void update() {

        /*if (startLabel != null) {
        // Animate fade-out
        final LabelWidget labelWidget = this.startLabel;
        labelWidget.setVisible(true);
        RequestProcessor.getDefault().post(new Runnable() {
        public void run() {
        final int Sleep = 200;
        final int Progress = 10;
        for (int i = 0; i < 255 / Progress + 1; i++) {
        try {
        SwingUtilities.invokeAndWait(new Runnable() {
        public void run() {
        Color c = labelWidget.getForeground();
        int v = c.getRed();
        v += Progress;
        if (v > 255) {
        v = 255;
        }
        labelWidget.setForeground(new Color(v, v, v, 255 - v));
        labelWidget.getScene().validate();
        }
        });
        } catch (InterruptedException ex) {
        } catch (InvocationTargetException ex) {
        }
        try {
        Thread.sleep(Sleep);
        } catch (InterruptedException ex) {
        }
        }
        labelWidget.setVisible(false);
        DiagramScene.this.scrollChangeListener.unregister(labelWidget);
        }
        }, 1000);
        startLabel = null;
        }*/

        slotLayer.removeChildren();
        mainLayer.removeChildren();
        blockLayer.removeChildren();

        blockWidgets.clear();
        figureWidgets = new HashMap<Figure, FigureWidget>();
        slotWidgets = new HashMap<Slot, SlotWidget>();
        connectionWidgets = new HashMap<Connection, ConnectionWidget>();

        WidgetAction selectAction = new ExtendedSelectAction(selectProvider);
        Diagram d = getModel().getDiagramToView();

        if (getModel().getShowBlocks()) {
            Scheduler s = Lookup.getDefault().lookup(Scheduler.class);
            Collection<InputBlock> newBlocks = new ArrayList<InputBlock>(s.schedule(d.getGraph()));
            d.schedule(newBlocks);
        }

        for (Figure f : d.getFigures()) {
            FigureWidget w = new FigureWidget(f, this, mainLayer);
            w.getActions().addAction(selectAction);
            w.getActions().addAction(hoverAction);
            w.getActions().addAction(ActionFactory.createPopupMenuAction(w));
            w.getActions().addAction(new DoubleClickAction(w));
            w.setVisible(false);

            figureWidgets.put(f, w);

            for (InputSlot s : f.getInputSlots()) {
                SlotWidget sw = new InputSlotWidget(s, this, slotLayer, w);
                slotWidgets.put(s, sw);
                sw.getActions().addAction(selectAction);
            }

            for (OutputSlot s : f.getOutputSlots()) {
                SlotWidget sw = new OutputSlotWidget(s, this, slotLayer, w);
                slotWidgets.put(s, sw);
                sw.getActions().addAction(selectAction);
            }
        }

        if (getModel().getShowBlocks()) {
            for (InputBlock bn : d.getGraph().getBlocks()) {
                BlockWidget w = new BlockWidget(this, d, bn);
                w.setVisible(false);
                blockWidgets.put(bn, w);
                blockLayer.addChild(w);
            }
        }

        this.smallUpdate(true);

    }

    private void smallUpdate(boolean relayout) {

        this.updateHiddenNodes(model.getHiddenNodes(), relayout);
        boolean b = this.getUndoRedoEnabled();
        this.setUndoRedoEnabled(false);
        this.setSelection(getModel().getSelectedNodes());
        this.setUndoRedoEnabled(b);
        this.validate();
    }

    private boolean isVisible(Connection c) {
        FigureWidget w1 = figureWidgets.get(c.getInputSlot().getFigure());
        FigureWidget w2 = figureWidgets.get(c.getOutputSlot().getFigure());

        if (w1.isVisible() && w2.isVisible()) {
            return true;
        }

        return false;
    }

    private void relayout(Set<Widget> oldVisibleWidgets) {

        Diagram diagram = getModel().getDiagramToView();

        HashSet<Figure> figures = new HashSet<Figure>();

        for (Figure f : diagram.getFigures()) {
            FigureWidget w = figureWidgets.get(f);
            if (w.isVisible()) {
                figures.add(f);
            }
        }

        HashSet<Connection> edges = new HashSet<Connection>();

        for (Connection c : diagram.getConnections()) {
            if (isVisible(c)) {
                edges.add(c);
            }
        }

        if (getModel().getShowBlocks()) {
            HierarchicalClusterLayoutManager m = new HierarchicalClusterLayoutManager(OldHierarchicalLayoutManager.Combine.SAME_OUTPUTS);
            HierarchicalLayoutManager manager = new HierarchicalLayoutManager(HierarchicalLayoutManager.Combine.SAME_OUTPUTS);
            manager.setMaxLayerLength(9);
            manager.setMinLayerDifference(3);
            m.setManager(manager);
            m.setSubManager(new HierarchicalLayoutManager(HierarchicalLayoutManager.Combine.SAME_OUTPUTS));
            m.doLayout(new LayoutGraph(edges, figures));

        } else {
            HierarchicalLayoutManager manager = new HierarchicalLayoutManager(HierarchicalLayoutManager.Combine.SAME_OUTPUTS);
            manager.setMaxLayerLength(10);
            manager.doLayout(new LayoutGraph(edges, figures));
        }

        int maxX = -BORDER_SIZE;
        int maxY = -BORDER_SIZE;
        for (Figure f : diagram.getFigures()) {
            FigureWidget w = figureWidgets.get(f);
            if (w.isVisible()) {
                Point p = f.getPosition();
                Dimension d = f.getSize();
                maxX = Math.max(maxX, p.x + d.width);
                maxY = Math.max(maxY, p.y + d.height);
            }
        }

        for (Connection c : diagram.getConnections()) {
            List<Point> points = c.getControlPoints();
            FigureWidget w1 = figureWidgets.get((Figure) c.getTo().getVertex());
            FigureWidget w2 = figureWidgets.get((Figure) c.getFrom().getVertex());
            if (w1.isVisible() && w2.isVisible()) {
                for (Point p : points) {
                    if (p != null) {
                        maxX = Math.max(maxX, p.x);
                        maxY = Math.max(maxY, p.y);
                    }
                }
            }
        }

        if (getModel().getShowBlocks()) {
            for (Block b : diagram.getBlocks()) {
                BlockWidget w = blockWidgets.get(b.getInputBlock());
                if (w != null && w.isVisible()) {
                    Rectangle r = b.getBounds();
                    maxX = Math.max(maxX, r.x + r.width);
                    maxY = Math.max(maxY, r.y + r.height);
                }
            }
        }

        bottomRight.setPreferredLocation(new Point(maxX + BORDER_SIZE, maxY + BORDER_SIZE));
        int offx = 0;
        int offy = 0;
        int curWidth = maxX + 2 * BORDER_SIZE;
        int curHeight = maxY + 2 * BORDER_SIZE;

        Rectangle bounds = this.getScrollPane().getBounds();
        if (curWidth < bounds.width) {
            offx = (bounds.width - curWidth) / 2;
        }

        if (curHeight < bounds.height) {
            offy = (bounds.height - curHeight) / 2;
        }

        final int offx2 = offx;
        final int offy2 = offy;

        SceneAnimator animator = this.getSceneAnimator();
        connectionLayer.removeChildren();
        int visibleFigureCount = 0;
        for (Figure f : diagram.getFigures()) {
            if (figureWidgets.get(f).isVisible()) {
                visibleFigureCount++;
            }
        }

        for (Figure f : diagram.getFigures()) {
            for (OutputSlot s : f.getOutputSlots()) {
                SceneAnimator anim = animator;
                if (visibleFigureCount > ANIMATION_LIMIT) {
                    anim = null;
                }
                processOutputSlot(s, s.getConnections(), 0, null, null, offx2, offy2, anim);
            }
        }

        for (Figure f : diagram.getFigures()) {
            FigureWidget w = figureWidgets.get(f);
            if (w.isVisible()) {
                Point p = f.getPosition();
                Point p2 = new Point(p.x + offx2, p.y + offy2);
                Rectangle r = new Rectangle(p.x + offx2, p.y + offy2, f.getSize().width, f.getSize().height);
                if (oldVisibleWidgets.contains(w)) {
                    if (visibleFigureCount > ANIMATION_LIMIT) {
                        w.setPreferredLocation(p2);
                    } else {
                        animator.animatePreferredLocation(w, p2);
                    }
                } else {
                    w.setPreferredLocation(p2);
                }
            }
        }

        if (getModel().getShowBlocks()) {
            for (Block b : diagram.getBlocks()) {
                BlockWidget w = blockWidgets.get(b.getInputBlock());
                if (w != null && w.isVisible()) {
                    Point location = new Point(b.getBounds().x + offx2, b.getBounds().y + offy2);
                    Rectangle r = new Rectangle(location.x, location.y, b.getBounds().width, b.getBounds().height);
                    if (oldVisibleWidgets.contains(w)) {
                        if (visibleFigureCount > ANIMATION_LIMIT) {
                            w.setPreferredBounds(r);
                        } else {
                            animator.animatePreferredBounds(w, r);
                        }
                    } else {
                        w.setPreferredBounds(r);
                    }
                }
            }
        }
    }
    private final Point specialNullPoint = new Point(Integer.MAX_VALUE, Integer.MAX_VALUE);

    private void processOutputSlot(OutputSlot s, List<Connection> connections, int controlPointIndex, Point lastPoint, LineWidget predecessor, int offx, int offy, SceneAnimator animator) {
        Map<Point, List<Connection>> pointMap = new HashMap<Point, List<Connection>>();

        for (Connection c : connections) {

            if (!isVisible(c)) {
                continue;
            }

            List<Point> controlPoints = c.getControlPoints();
            if (controlPointIndex >= controlPoints.size()) {
                continue;
            }

            Point cur = controlPoints.get(controlPointIndex);
            if (cur == null) {
                cur = specialNullPoint;
            } else if (controlPointIndex == 0 && !s.getShowName()) {
                cur = new Point(cur.x, cur.y - SLOT_OFFSET);
            } else if (controlPointIndex == controlPoints.size() - 1 && !c.getInputSlot().getShowName()) {
                cur = new Point(cur.x, cur.y + SLOT_OFFSET);
            }

            if (pointMap.containsKey(cur)) {
                pointMap.get(cur).add(c);
            } else {
                List<Connection> newList = new ArrayList<Connection>(2);
                newList.add(c);
                pointMap.put(cur, newList);
            }

        }

        for (Point p : pointMap.keySet()) {
            List<Connection> connectionList = pointMap.get(p);

            boolean isBold = false;
            boolean isDashed = true;

            for (Connection c : connectionList) {

                if (c.getStyle() == Connection.ConnectionStyle.BOLD) {
                    isBold = true;
                }

                if (c.getStyle() != Connection.ConnectionStyle.DASHED) {
                    isDashed = false;
                }
            }

            LineWidget newPredecessor = predecessor;
            if (p == specialNullPoint) {

            } else if (lastPoint == specialNullPoint) {

            } else if (lastPoint != null) {
                Point p1 = new Point(lastPoint.x + offx, lastPoint.y + offy);
                Point p2 = new Point(p.x + offx, p.y + offy);
                LineWidget w = new LineWidget(this, s, connectionList, p1, p2, predecessor, animator, isBold, isDashed);
                newPredecessor = w;
                connectionLayer.addChild(w);
                w.getActions().addAction(hoverAction);
            }

            processOutputSlot(s, connectionList, controlPointIndex + 1, p, newPredecessor, offx, offy, animator);
        }
    }

    private void clearSelection() {
        if (selectedWidgets.size() == 0) {
            return;
        }
        for (FigureWidget w : selectedWidgets) {
            assert w.getState().isSelected();
            w.setState(w.getState().deriveSelected(false));
            content.remove(w.getNode());
        }
        selectedWidgets.clear();
    }

    public Lookup getLookup() {
        return lookup;
    }

    public void gotoFigures(final List<Figure> figures) {
        Rectangle overall = null;
        showFigures(figures);
        for (Figure f : figures) {

            FigureWidget fw = getFigureWidget(f);
            if (fw != null) {
                Rectangle r = fw.getBounds();
                Point p = fw.getLocation();
                Rectangle r2 = new Rectangle(p.x, p.y, r.width, r.height);

                if (overall == null) {
                    overall = r2;
                } else {
                    overall = overall.union(r2);
                }
            }
        }
        if (overall != null) {
            centerRectangle(overall);
        }
    }

    private Point calcCenter(Rectangle r) {

        Point center = new Point((int) r.getCenterX(), (int) r.getCenterY());
        center.x -= getScrollPane().getViewport().getViewRect().width / 2;
        center.y -= getScrollPane().getViewport().getViewRect().height / 2;

        // Ensure to be within area
        center.x = Math.max(0, center.x);
        center.x = Math.min(getScrollPane().getViewport().getViewSize().width - getScrollPane().getViewport().getViewRect().width, center.x);
        center.y = Math.max(0, center.y);
        center.y = Math.min(getScrollPane().getViewport().getViewSize().height - getScrollPane().getViewport().getViewRect().height, center.y);

        return center;
    }

    private void centerRectangle(Rectangle r) {

        if (getScrollPane().getViewport().getViewRect().width == 0 || getScrollPane().getViewport().getViewRect().height == 0) {
            return;
        }

        Rectangle r2 = new Rectangle(r.x, r.y, r.width, r.height);
        r2 = convertSceneToView(r2);

        double factorX = (double) r2.width / (double) getScrollPane().getViewport().getViewRect().width;
        double factorY = (double) r2.height / (double) getScrollPane().getViewport().getViewRect().height;
        double factor = Math.max(factorX, factorY);
        if (factor >= 1.0) {
            Point p = getScrollPane().getViewport().getViewPosition();
            setZoomFactor(getZoomFactor() / factor);
            r2.x /= factor;
            r2.y /= factor;
            r2.width /= factor;
            r2.height /= factor;
            getScrollPane().getViewport().setViewPosition(calcCenter(r2));
        } else {
            getScrollPane().getViewport().setViewPosition(calcCenter(r2));
        }
    }

    private void addToSelection(Figure f) {
        FigureWidget w = getFigureWidget(f);
        addToSelection(w);
    }

    private void addToSelection(FigureWidget w) {
        assert !selectedWidgets.contains(w);
        selectedWidgets.add(w);
        content.add(w.getNode());
        w.setState(w.getState().deriveSelected(true));
    }

    private void setSelection(Set<Integer> nodes) {
        clearSelection();
        for (Figure f : getModel().getDiagramToView().getFigures()) {
            if (doesIntersect(f.getSource().getSourceNodesAsSet(), nodes)) {
                addToSelection(f);
            }
        }
        selectionUpdated();
        this.validate();
    }

    public void setSelection(Collection<Figure> list) {
        clearSelection();
        for (Figure f : list) {
            addToSelection(f);
        }

        selectionUpdated();
        this.validate();
    }

    public Set<Figure> getSelectedFigures() {
        Set<Figure> result = new HashSet<Figure>();
        for (Widget w : selectedWidgets) {
            if (w instanceof FigureWidget) {
                FigureWidget fw = (FigureWidget) w;
                if (fw.getState().isSelected()) {
                    result.add(fw.getFigure());
                }
            }
        }
        return result;
    }

    public Set<Integer> getSelectedNodes() {
        Set<Integer> result = new HashSet<Integer>();
        for (Widget w : selectedWidgets) {
            if (w instanceof FigureWidget) {
                FigureWidget fw = (FigureWidget) w;
                if (fw.getState().isSelected()) {
                    result.addAll(fw.getFigure().getSource().getSourceNodesAsSet());
                }
            }
        }
        return result;
    }

    private UndoRedo.Manager getUndoRedoManager() {
        if (undoRedoManager == null) {
            undoRedoManager = new UndoRedo.Manager();
            undoRedoManager.setLimit(UNDOREDO_LIMIT);
        }

        return undoRedoManager;
    }

    public UndoRedo getUndoRedo() {
        return getUndoRedoManager();
    }

    private boolean isVisible(Figure f) {
        for (Integer n : f.getSource().getSourceNodesAsSet()) {
            if (getModel().getHiddenNodes().contains(n)) {
                return false;
            }
        }
        return true;
    }

    private boolean doesIntersect(Set s1, Set s2) {
        if (s1.size() > s2.size()) {
            Set tmp = s1;
            s1 = s2;
            s2 = tmp;
        }

        for (Object o : s1) {
            if (s2.contains(o)) {
                return true;
            }
        }

        return false;
    }

    public void showNot(final Set<Integer> nodes) {
        updateHiddenNodes(nodes, true);
    }

    public void showOnly(final Set<Integer> nodes) {
        HashSet<Integer> allNodes = new HashSet<Integer>(getModel().getGraphToView().getGroup().getAllNodes());
        allNodes.removeAll(nodes);
        updateHiddenNodes(allNodes, true);
    }

    private void updateHiddenNodes(Set<Integer> newHiddenNodes, boolean doRelayout) {

        Set<InputBlock> visibleBlocks = new HashSet<InputBlock>();

        Diagram diagram = getModel().getDiagramToView();
        assert diagram != null;

        Set<Widget> oldVisibleWidgets = new HashSet<Widget>();

        for (Figure f : diagram.getFigures()) {
            FigureWidget w = figureWidgets.get(f);
            if (w.isVisible()) {
                oldVisibleWidgets.add(w);
            }
        }

        if (getModel().getShowBlocks()) {
            for (InputBlock b : diagram.getGraph().getBlocks()) {
                BlockWidget w = blockWidgets.get(b);
                if (w.isVisible()) {
                    oldVisibleWidgets.add(w);
                }
            }
        }

        for (Figure f : diagram.getFigures()) {
            boolean hiddenAfter = doesIntersect(f.getSource().getSourceNodesAsSet(), newHiddenNodes);

            FigureWidget w = this.figureWidgets.get(f);
            w.setBoundary(false);
            if (!hiddenAfter) {
                // Figure is shown
                w.setVisible(true);
                for (InputNode n : f.getSource().getSourceNodes()) {
                    visibleBlocks.add(diagram.getGraph().getBlock(n));
                }
            } else {
                // Figure is hidden
                w.setVisible(false);
            }
        }

        if (getModel().getShowNodeHull()) {
            List<FigureWidget> boundaries = new ArrayList<FigureWidget>();
            for (Figure f : diagram.getFigures()) {
                FigureWidget w = this.figureWidgets.get(f);
                if (!w.isVisible()) {
                    Set<Figure> set = new HashSet<Figure>(f.getPredecessorSet());
                    set.addAll(f.getSuccessorSet());

                    boolean b = false;
                    for (Figure neighbor : set) {
                        FigureWidget neighborWidget = figureWidgets.get(neighbor);
                        if (neighborWidget.isVisible()) {
                            b = true;
                            break;
                        }
                    }

                    if (b) {
                        w.setBoundary(true);
                        for (InputNode n : f.getSource().getSourceNodes()) {
                            visibleBlocks.add(diagram.getGraph().getBlock(n));
                        }
                        boundaries.add(w);
                    }
                }
            }

            for (FigureWidget w : boundaries) {
                if (w.isBoundary()) {
                    w.setVisible(true);
                }
            }
        }

        if (getModel().getShowBlocks()) {
            for (InputBlock b : diagram.getGraph().getBlocks()) {

                boolean visibleAfter = visibleBlocks.contains(b);

                BlockWidget w = blockWidgets.get(b);
                if (visibleAfter) {
                    // Block must be shown
                    w.setVisible(true);
                } else {
                    // Block must be hidden
                    w.setVisible(false);
                }
            }
        }

        getModel().setHiddenNodes(newHiddenNodes);
        if (doRelayout) {
            relayout(oldVisibleWidgets);
        }
        this.validate();
        addUndo();
    }

    private void showFigures(Collection<Figure> f) {
        HashSet<Integer> newHiddenNodes = new HashSet<Integer>(getModel().getHiddenNodes());
        for (Figure fig : f) {
            newHiddenNodes.removeAll(fig.getSource().getSourceNodesAsSet());
        }
        updateHiddenNodes(newHiddenNodes, true);
    }

    private void showFigure(Figure f) {
        HashSet<Integer> newHiddenNodes = new HashSet<Integer>(getModel().getHiddenNodes());
        newHiddenNodes.removeAll(f.getSource().getSourceNodesAsSet());
        updateHiddenNodes(newHiddenNodes, true);
    }

    public void showAll(final Collection<Figure> f) {
        showFigures(f);

    }

    public void show(final Figure f) {
        showFigure(f);
    }

    public void gotoFigure(final Figure f) {

        if (!isVisible(f)) {
            showFigure(f);
        }

        FigureWidget fw = getFigureWidget(f);
        if (fw != null) {
            Rectangle r = fw.getBounds();
            Point p = fw.getLocation();
            centerRectangle(new Rectangle(p.x, p.y, r.width, r.height));

            // Select figure
            clearSelection();
            addToSelection(fw);
            selectionUpdated();
        }
    }

    public JPopupMenu createPopupMenu() {
        JPopupMenu menu = new JPopupMenu();
        for (Action a : actions) {
            if (a == null) {
                menu.addSeparator();
            } else {
                menu.add(a);
            }
        }
        return menu;
    }

    private static class DiagramUndoRedo extends AbstractUndoableEdit implements ChangedListener<DiagramViewModel> {

        private DiagramViewModel oldModel;
        private DiagramViewModel newModel;
        private Point oldScrollPosition;
        private DiagramScene scene;

        public DiagramUndoRedo(DiagramScene scene, Point oldScrollPosition, DiagramViewModel oldModel, DiagramViewModel newModel) {
            assert oldModel != null;
            assert newModel != null;
            this.oldModel = oldModel;
            this.newModel = newModel;
            this.scene = scene;
            this.oldScrollPosition = oldScrollPosition;
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            boolean b = scene.getUndoRedoEnabled();
            scene.setUndoRedoEnabled(false);
            scene.getModel().getViewChangedEvent().addListener(this);
            scene.getModel().setData(newModel);
            scene.getModel().getViewChangedEvent().removeListener(this);
            scene.setUndoRedoEnabled(b);
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            boolean b = scene.getUndoRedoEnabled();
            scene.setUndoRedoEnabled(false);
            scene.getModel().getViewChangedEvent().addListener(this);
            scene.getModel().setData(oldModel);
            scene.getModel().getViewChangedEvent().removeListener(this);

            SwingUtilities.invokeLater(new Runnable() {

                public void run() {
                    scene.setScrollPosition(oldScrollPosition);
                }
            });

            scene.setUndoRedoEnabled(b);
        }

        public void changed(DiagramViewModel source) {
            scene.getModel().getViewChangedEvent().removeListener(this);
            if (oldModel.getSelectedNodes().equals(newModel.getHiddenNodes())) {
                scene.smallUpdate(false);
            } else {
                scene.smallUpdate(true);
            }
        }
    }
    private boolean undoRedoEnabled = true;

    public void setUndoRedoEnabled(boolean b) {
        this.undoRedoEnabled = b;
    }

    public boolean getUndoRedoEnabled() {
        return undoRedoEnabled;
    }

    public void changed(DiagramViewModel source) {
        assert source == model : "Receive only changed event from current model!";
        assert source != null;
        update();
    }

    private void addUndo() {

        DiagramViewModel newModelCopy = model.copy();

        if (undoRedoEnabled) {
            this.getUndoRedoManager().undoableEditHappened(new UndoableEditEvent(this, new DiagramUndoRedo(this, this.getScrollPosition(), modelCopy, newModelCopy)));
        }

        this.modelCopy = newModelCopy;
    }
}
