/*
 * ARX Data Anonymization Tool
 * Copyright 2012 - 2024 Fabian Prasser and contributors
 * 
 * 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 org.deidentifier.arx.gui.view.impl.explore;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.deidentifier.arx.ARXLattice;
import org.deidentifier.arx.ARXLattice.ARXNode;
import org.deidentifier.arx.ARXResult;
import org.deidentifier.arx.gui.Controller;
import org.deidentifier.arx.gui.model.ModelNodeFilter;
import org.deidentifier.arx.gui.resources.Resources;
import org.deidentifier.arx.gui.view.SWTUtil;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
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.Path;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.graphics.Transform;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;

/**
 * This class implements a view of a lattice.
 *
 * @author Fabian Prasser
 */
public class ViewLattice extends ViewSolutionSpace {

    /**
     * This class is here for backwards compatibility only.
     */
    @SuppressWarnings("unused")
    private static class Bounds implements Serializable {
        
        /**  SVUID */
        private static final long serialVersionUID = -7472570696920782588L;
    }
    
    /**
     * The current drag type.
     */
    private static enum DragType {
        
        /**  MOVE */
        MOVE,
        
        /**  ZOOM */
        ZOOM,
        
        /**  NONE */
        NONE
    }

    /**
     * This class is here for serializability, only.
     */
    private static class SerializablePath implements Serializable {
        
        /**  SVUID */
        private static final long serialVersionUID = -4572722688452678425L;
        
        /**  Path */
        private final transient Path path;
        
        /** Constructor
         * @param path
         */
        public SerializablePath(Path path){
            this.path = path;
        }
        
        /**
         * Dispose
         */
        public void dispose(){
            if (!this.path.isDisposed()) {
                this.path.dispose();
            }
        }
        
        /**
         * Returns the path
         * @return
         */
        public Path getPath(){
            return this.path;
        }
    }

    /** Color. */
    private static final Color        COLOR_BLACK             = GUIHelper.getColor(0, 0, 0);

    /** Color. */
    private static final Color        COLOR_LIGHT_GRAY        = GUIHelper.getColor(211, 211, 211);

    /** Color. */
    private static final Color        COLOR_LINE              = GUIHelper.getColor(200, 200, 200);

    /** Attribute constant. */
    private static final int          ATTRIBUTE_CENTER        = 4;
    
    /** Attribute constant. */
    private static final int          ATTRIBUTE_LABEL         = 5;
    
    /** Attribute constant. */
    private static final int          ATTRIBUTE_VISIBLE       = 6;
    
    /** Attribute constant. */
    private static final int          ATTRIBUTE_PATH          = 7;
    
    /** Attribute constant. */
    private static final int          ATTRIBUTE_EXTENT        = 8;

    /** Time to wait for a tool tip to show. */
    private static final int          TOOLTIP_WAIT            = 200;
    
    /** Global settings. */
    private static final double       NODE_INITIAL_SIZE       = 200d;
    
    /** Global settings. */
    private static final double       NODE_FRAME_RATIO        = 0.7d;
    
    /** Global settings. */
    private static final double       NODE_SIZE_RATIO         = 0.3d;
    
    /** Global settings. */
    private static final double       ZOOM_SPEED              = 10d;
    
    /** Global settings. */
    private static final int          MSG_WIDTH               = 300;
    
    /** Global settings. */
    private static final int          MSG_HEIGHT              = 100;
    
    /** Global settings. */
    private static final int          MIN_WIDTH               = 2;
    
    /** Global settings. */
    private static final int          MIN_HEIGHT              = 1;
    
    /** For the current view. */
    private static final int          STROKE_WIDTH_NODE       = 1;
    
    /** For the current view. */
    private static final int          STROKE_WIDTH_CONNECTION = 1;
    
    /** The font. */
    private Font                      font                    = null;

    /** For the current view. */
    private double                    nodeWidth               = 0f;
    
    /** For the current view. */
    private double                    nodeHeight              = 0f;
    
    /** The lattice to display. */
    private final List<List<ARXNode>> lattice                 = new ArrayList<List<ARXNode>>();
    
    /** The according ARX lattice. */
    private ARXLattice                arxLattice              = null;
    
    /** The lattice to display. */
    private int                       latticeWidth            = 0;
    
    /** The screen size. */
    private Point                     screen                  = null;

    /** The number of nodes. */
    private int                       numNodes                = 0;
    
    /** Drag parameters. */
    private int                       dragX                   = 0;
    
    /** Drag parameters. */
    private int                       dragY                   = 0;
    
    /** Drag parameters. */
    private int                       dragStartX              = 0;
    
    /** Drag parameters. */
    private int                       dragStartY              = 0;
    
    /** Drag parameters. */
    private DragType                  dragType                = DragType.NONE;

    /** The tool tip. */
    private int                       tooltipX                = -1;
    
    /** The tool tip. */
    private int                       tooltipY                = -1;
    
    /** The tool tip. */
    private int                       oldTooltipX             = -1;
    
    /** The tool tip. */
    private int                       oldTooltipY             = -1;
    
    /** The canvas. */
    private final Canvas              canvas;

    /**
     * Creates a new instance.
     *
     * @param parent
     * @param controller
     */
    public ViewLattice(final Composite parent, final Controller controller) {

        super(parent, controller);
                
        // Compute font
        FontData[] fd = parent.getFont().getFontData();
        fd[0].setHeight(8);
        this.font = new Font(parent.getDisplay(), fd[0]);

        // Build canvas
        this.canvas = new Canvas(getPrimaryComposite(), SWT.DOUBLE_BUFFERED);
        this.canvas.setLayoutData(SWTUtil.createFillGridData());
        this.canvas.addPaintListener(new PaintListener() {
            @Override
            public void paintControl(PaintEvent e) {
                screen = canvas.getSize();
                e.gc.setAdvanced(true);
                e.gc.setAntialias(SWT.ON);
                draw(e.gc);
            }
        });
        this.canvas.addDisposeListener(new DisposeListener(){
            public void widgetDisposed(DisposeEvent arg0) {
                clearLatticeAndDisposePaths(); // Free resources
            }
        });
        
        // Initialize
        this.initializeToolTipTimer();
        this.initializeListeners();
    }

    @Override
    public void dispose() {
        super.dispose();
        font.dispose();
    }

    /**
     * Resets the view.
     */
    @Override
    public void reset() {
        super.reset();
        this.numNodes = 0;
        this.arxLattice = null;
        this.clearLatticeAndDisposePaths();
        this.latticeWidth = 0;
        this.screen = null;
        this.canvas.redraw();
    }

    /**
     * Called when button 1 is clicked on a node.
     *
     * @param node
     */
    private void actionButtonClicked1(ARXNode node) {
        actionSelectNode(node);
        canvas.redraw();
    }

    /**
     * Called when button 3 is clicked on a node.
     *
     * @param node
     * @param x
     * @param y
     */
    private void actionButtonClicked3(ARXNode node, final int x, final int y) {
        actionSelectNode(node);
        canvas.redraw();
        actionShowMenu(x, y);
        dragType = DragType.NONE;
    }

    /**
     * Clears the lattice.
     */
    private void clearLatticeAndDisposePaths() {
        for (List<ARXNode> level : lattice) {
            for (ARXNode node : level) {
                SerializablePath path = (SerializablePath)node.getAttributes().get(ATTRIBUTE_PATH);
                if (path!=null && path.getPath()!=null) {
                    node.getAttributes().put(ATTRIBUTE_PATH, null);
                    path.dispose();
                }
            }
        }
        this.lattice.clear();
    }

    /**
     * Draws the lattice.
     *
     * @param g
     */
    private void draw(final GC g) {

        // Fill background
        Point size = canvas.getSize();
        g.setBackground(g.getDevice().getSystemColor(SWT.COLOR_LIST_BACKGROUND));
        g.fillRectangle(0, 0, size.x, size.y);
                
        // Return, if nothing to show
        if (getModel() == null) {

            // Draw border
            g.setForeground(g.getDevice().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
            g.drawRectangle(0, 0, screen.x-1, screen.y-1);
            return;
        }

        // If too many nodes
        if (numNodes > getModel().getMaxNodesInViewer()) {
            int x = (size.x / 2) - (MSG_WIDTH / 2);
            int y = (size.y / 2) - (MSG_HEIGHT / 2);
            if ((x < 0) || (y < 0)) { return; }
            g.setBackground(COLOR_LIGHT_GRAY);
            g.fillRectangle(x, y, MSG_WIDTH, MSG_HEIGHT);
            g.setForeground(COLOR_BLACK);
            g.drawRectangle(x, y, MSG_WIDTH, MSG_HEIGHT);
            drawText(g, Resources.getMessage("LatticeView.7"), x, y, MSG_WIDTH, MSG_HEIGHT); //$NON-NLS-1$
            return;
        }

        // Return, if nothing to show
        if (lattice.isEmpty() || (screen == null)) { return; }

        // Draw connections
        drawConnections(g);
        
        // Draw nodes
        drawNodes(g);
        
        // Draw border
        g.setForeground(g.getDevice().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
        g.drawRectangle(0, 0, screen.x-1, screen.y-1);
    }

    /**
     * Draws the connections.
     *
     * @param g
     */
    private void drawConnections(GC g) {
        
        // Prepare
        Set<ARXNode> done = new HashSet<ARXNode>();
        int[] clip = new int[4];

        // Set style
        g.setLineWidth(STROKE_WIDTH_CONNECTION);
        g.setForeground(COLOR_LINE);

        // For each node
        for (List<ARXNode> level : lattice) {
            for (ARXNode node1 : level) {

                // Obtain coordinates
                double[] center1 = (double[]) node1.getAttributes().get(ATTRIBUTE_CENTER);
                
                // Draw
                for (final ARXNode node2 : node1.getSuccessors()) {
                    
                    boolean visible = (Boolean)node2.getAttributes().get(ATTRIBUTE_VISIBLE);
                    if (visible && !done.contains(node2)) {

                       // Obtain coordinates
                       double[] center2 = (double[]) node2.getAttributes().get(ATTRIBUTE_CENTER);
                           
                       // Perform clipping
                       if (liangBarsky(0, screen.x, 0, screen.y, 
                                       center1[0], center1[1],
                                       center2[0], center2[1],
                                       clip)) {
                           
                           // Draw
                           g.drawLine(clip[0], clip[1], clip[2], clip[3]);
                       }
                    }
                }

                // Add to set of already processed nodes
                done.add(node1);
            }
        }
    }

    /**
     * Draws a node.
     *
     * @param g
     */
    private void drawNodes(final GC g) {

        // Prepare
        Rectangle bounds = new Rectangle(0, 0, (int)nodeWidth, (int)nodeHeight);
        Transform transform = new Transform(g.getDevice());
        
        // Set style
        g.setLineWidth(STROKE_WIDTH_NODE);
        g.setFont(font);

        // Draw nodes
        for (List<ARXNode> level : lattice) {
            for (ARXNode node : level) {
                
                // Obtain coordinates
                double[] center = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
                bounds.x = (int)(center[0] - nodeWidth / 2d);
                bounds.y = (int)(center[1] - nodeHeight / 2d);
                
                // Clipping
                if (bounds.intersects(new Rectangle(0, 0, screen.x, screen.y))) { 
                    
                    // Retrieve/compute text rendering data
                    SerializablePath path = (SerializablePath) node.getAttributes().get(ATTRIBUTE_PATH);
                    Point extent = (Point) node.getAttributes().get(ATTRIBUTE_EXTENT);
                    if (path == null || path.getPath() == null) {
                        String text = (String) node.getAttributes().get(ATTRIBUTE_LABEL);
                        path = new SerializablePath(new Path(canvas.getDisplay()));
                        path.getPath().addString(text, 0, 0, font);
                        node.getAttributes().put(ATTRIBUTE_PATH, path);
                        extent = g.textExtent(text);
                        node.getAttributes().put(ATTRIBUTE_EXTENT, extent);
                    }
            
                    // Degrade if too far away
                    if (bounds.width <= 4) {
                        g.setBackground(getInnerColor(node));
                        g.setAntialias(SWT.OFF);
                        g.fillRectangle(bounds.x, bounds.y, bounds.width, bounds.height);
            
                        // Draw real node
                    } else {
                        
                        // Fill background
                        g.setBackground(getInnerColor(node));
                        g.setAntialias(SWT.OFF);
                        if (node != getSelectedNode()) {
                            g.fillOval(bounds.x, bounds.y, bounds.width, bounds.height);
                        } else {
                            g.fillRectangle(bounds.x, bounds.y, bounds.width, bounds.height);
                        }
                        
                        // Draw line
                        g.setLineWidth(getOuterStrokeWidth(node, bounds.width));
                        g.setForeground(getOuterColor(node));
                        g.setAntialias(SWT.ON);
                        if (node != getSelectedNode()) {
                            g.drawOval(bounds.x, bounds.y, bounds.width, bounds.height);
                        } else {
                            g.drawRectangle(bounds.x, bounds.y, bounds.width, bounds.height);
                        }
                        
                        // Draw text
                        if (bounds.width >= 20) {
                            
                            // Enable anti-aliasing
                            g.setTextAntialias(SWT.ON);
                            
                            // Compute position and factor
                            float factor1 = (bounds.width * 0.7f) / (float)extent.x;
                            float factor2 = (bounds.height * 0.7f) / (float)extent.y;
                            float factor = Math.min(factor1, factor2);
                            int positionX = bounds.x + (int)(((float)bounds.width - (float)extent.x * factor) / 2f); 
                            int positionY = bounds.y + (int)(((float)bounds.height - (float)extent.y * factor) / 2f);
                            
                            // Initialize transformation
                            transform.identity();
                            transform.translate(positionX, positionY);
                            transform.scale(factor, factor);
                            g.setTransform(transform);
                            
                            // Draw and reset
                            g.setBackground(COLOR_BLACK);
                            g.fillPath(path.getPath());
                            g.setTransform(null);
                            g.setTextAntialias(SWT.OFF);
                        }
                    }
                }
            }
        }
        
        // Clean up
        transform.dispose();
    }

    /**
     * Utility method which centers a text in a rectangle.
     *
     * @param gc
     * @param text
     * @param x
     * @param y
     * @param width
     * @param height
     */
    private void drawText(final GC gc, final String text, final int x, final int y, final int width, final int height) {

        Point size = canvas.getSize();
        Point extent = gc.textExtent(text);
        gc.setClipping(x, y, width, height);
        int xx = x + (width - extent.x) / 2;
        int yy = y + height / 2 - extent.y / 2;
        gc.drawText(text, xx, yy, true);
        gc.setClipping(0, 0, size.x, size.y);
    }

    /**
     * Returns the node at the given location.
     *
     * @param x
     * @param y
     * @return
     */
    private ARXNode getNode(final int x, final int y) {
        for (List<ARXNode> level : lattice) {
            for (ARXNode node : level) {
                double[] bounds = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
                if (bounds == null) { return null; }
                if ((x >= bounds[0] - nodeHeight/2) && 
                    (y >= bounds[1] - nodeHeight/2) && 
                    (x <= (bounds[0] + nodeWidth/2)) && 
                    (y <= (bounds[1] + nodeHeight/2))) {
                    
                    return node; 
                }
            }
        }
        return null;
    }

    /**
     * Initializes the data structures for displaying a new lattice.
     *
     * @param result
     * @param filter
     */
    private void initialize(final ARXResult result, final ModelNodeFilter filter) {

        // Return if nothing to do
        if ((result == null) || (result.getLattice() == null) || (filter == null)) {
            reset();
            return;
        }

        // Obtain lattice
        final ARXLattice lattice = getModel().getProcessStatistics().isLocalTransformation() ? 
                                   getModel().getProcessStatistics().getLattice() : result.getLattice();
        
        // Clear the lattice
        if (!lattice.equals(this.arxLattice)) {
            this.clearLatticeAndDisposePaths();
            this.arxLattice = lattice;
        } else {
            this.lattice.clear();
        }

        // Build the visible sub-lattice
        this.latticeWidth = 0;
        this.numNodes = 0;
        for (ARXNode[] originalLevel : lattice.getLevels()) {
            List<ARXNode> level = new ArrayList<ARXNode>();
            for (ARXNode node : originalLevel) {
                boolean visible = filter.isAllowed(lattice, node);
                node.getAttributes().put(ATTRIBUTE_VISIBLE, visible);
                if (visible) {
                    level.add(node);
                    numNodes++;
                } 
            }
            if (!level.isEmpty()) {
                this.lattice.add(level);
            }
            this.latticeWidth = Math.max(this.latticeWidth, level.size());
        }

        // Check
        if (numNodes > getModel().getMaxNodesInViewer()) { return; }

        // Now initialize the text attribute
        for (List<ARXNode> level : this.lattice) {
            for (ARXNode node : level) {
                if (!node.getAttributes().containsKey(ATTRIBUTE_LABEL)) {
                    String text = Arrays.toString(node.getTransformation());
                    text = text.substring(1, text.length() - 1);
                    text = super.trimLabel(text);
                    node.getAttributes().put(ATTRIBUTE_LABEL, text);
                }
            }
        }
        
        // Reset the parameters
        initializeCanvas();
    }

    /**
     * Recomputes the initial positions of all nodes.
     */
    private void initializeCanvas() {

        // Obtain screen size
        screen = canvas.getSize();

        // Obtain optimal width and height per node
        double width = NODE_INITIAL_SIZE;
        double height = width * NODE_SIZE_RATIO;
        if ((height * lattice.size()) > screen.y) {
            double factor = screen.y / (height * lattice.size());
            height *= factor; width *= factor;
        }
        if ((width * latticeWidth) > screen.x) {
            double factor = screen.x / (width * latticeWidth);
            height *= factor; width *= factor;
        }
        nodeWidth = width * NODE_FRAME_RATIO;
        nodeHeight = height * NODE_FRAME_RATIO;

        // Compute deltas to center the lattice
        final double deltaY = (screen.y - (height * lattice.size())) / 2d;
        final double deltaX = (screen.x - (width * latticeWidth)) / 2d;

        // For each level
        double positionY = lattice.size() -1;
        for (List<ARXNode> level : lattice) {

            // For each node on this level
            double centerY = deltaY + (positionY * height) + (height / 2d);
            double positionX = 0;
            for (ARXNode node : level) {
    
                // Create and store node properties
                double offset = (latticeWidth * width) - (level.size() * width);
                double centerX = deltaX + (positionX * width) + (width / 2d) + (offset / 2d);
                node.getAttributes().put(ATTRIBUTE_CENTER, new double[]{centerX, centerY});
                
                // Next node
                positionX++;
            }
            // Next level
            positionY--;
        }
    }

    /**
     * Creates all required listeners.
     */
    private void initializeListeners() {

        canvas.addMouseListener(new MouseAdapter(){

            @Override
            public void mouseDown(MouseEvent arg0) {
                dragX = arg0.x;
                dragY = arg0.y;
                dragStartX = arg0.x;
                dragStartY = arg0.y;
                if (dragType == DragType.NONE) {
                    if (arg0.button == 1) {
                        dragType = DragType.MOVE;
                    } else  if (arg0.button == 3) {
                        dragType = DragType.ZOOM;
                    }
                }
            }

            @Override
            public void mouseUp(MouseEvent arg0) {
                dragType = DragType.NONE;
            }
        });

        canvas.addMouseListener(new MouseAdapter(){
            /** Drag parameters */
            private int clickX = 0;
            /** Drag parameters */
            private int clickY = 0;
            @Override
            public void mouseDown(MouseEvent arg0) {
                
                clickX = arg0.x;
                clickY = arg0.y;
                
                if (arg0.button == 1) {
                    final ARXNode node = getNode(arg0.x, arg0.y);
                    if (node != null) {
                        actionButtonClicked1(node);
                    }
                } 
            }
            @Override
            public void mouseUp(MouseEvent arg0) {
                if (arg0.button == 3 && arg0.x == clickX && arg0.y == clickY) {
                    final ARXNode node = getNode(arg0.x, arg0.y);
                    if (node != null) {
                        Point display = canvas.toDisplay(arg0.x, arg0.y);
                        actionButtonClicked3(node, display.x, display.y);
                    }
                }
                clickX = arg0.x;
                clickY = arg0.y;
            }       
            
        });

        canvas.addMouseMoveListener(new MouseMoveListener(){

            @Override
            public void mouseMove(MouseEvent arg0) {
                
                if (dragType != DragType.NONE) {
                    final int deltaX = arg0.x - dragX;
                    final int deltaY = arg0.y - dragY;
                    if (dragType == DragType.MOVE) {
                        
                        // Just move the nodes around
                        for (List<ARXNode> level : lattice) {
                            for (ARXNode node : level) {
                                double[] center = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
                                center[0] += deltaX;
                                center[1] += deltaY;
                            }
                        }
                        
                    } else if (dragType == DragType.ZOOM) {

                        // Compute zoom
                        double zoom = -((double) deltaY / (double) screen.y) * ZOOM_SPEED;
                        double newWidth = nodeWidth + (zoom * nodeWidth);
                        double newHeight = nodeHeight + (zoom * nodeHeight);

                        // Adjust
                        zoom = newWidth > screen.x ? (screen.x - nodeWidth) / nodeWidth : zoom;
                        zoom = newWidth < MIN_WIDTH ? (MIN_WIDTH - nodeWidth) / nodeWidth : zoom;                        
                        zoom = newHeight > screen.y ? (screen.y - nodeHeight) / nodeHeight : zoom;
                        zoom = newHeight < MIN_HEIGHT ? (MIN_HEIGHT - nodeHeight) / nodeHeight : zoom;

                        // Zoom the node size
                        nodeWidth += zoom * nodeWidth;
                        nodeHeight += zoom * nodeHeight;

                        // Zoom the node positions
                        for (List<ARXNode> level : lattice) {
                            for (ARXNode node : level) {
                                double[] center = (double[]) node.getAttributes().get(ATTRIBUTE_CENTER);
                                center[0] -= dragStartX;
                                center[0] += zoom * center[0];
                                center[0] += dragStartX;
    
                                center[1] -= dragStartY;
                                center[1] += zoom * center[1];
                                center[1] += dragStartY;
                            }
                        }
                    }
                    
                    // Store mouse data & redraw
                    dragX += deltaX;
                    dragY += deltaY;
                    canvas.redraw();
                }
            }
        });

        canvas.addMouseMoveListener(new MouseMoveListener(){
            public void mouseMove(MouseEvent arg0) {
                tooltipX = arg0.x;
                tooltipY = arg0.y;
            }
        });
        
        canvas.addListener(SWT.MouseExit, new Listener() {
            public void handleEvent(Event e) {
                tooltipX = -1;
                tooltipY = -1;
            }
          });
        
        canvas.addControlListener(new ControlAdapter() {

            @Override
            public void controlResized(ControlEvent arg0) {
                screen = canvas.getSize();
                initializeCanvas();
                canvas.redraw();
            }
        });
    }

    /**
     * For performance reasons, we check for tool tips only at certain times.
     */
    private void initializeToolTipTimer() {
        canvas.getDisplay().timerExec(TOOLTIP_WAIT, new Runnable(){
            public void run(){
                
                if (tooltipX != oldTooltipX || tooltipY != oldTooltipY) {
                    String text = null;
                    if (tooltipX != -1 && tooltipY != -1) {
                        ARXNode node = getNode(tooltipX, tooltipY);
                        text = node == null ? null : getTooltipDecorator().decorate(node);
                    } 
                    canvas.setToolTipText(text);
                }
                oldTooltipX = tooltipX;
                oldTooltipY = tooltipY;
                if (!canvas.isDisposed()) {
                    canvas.getDisplay().timerExec(TOOLTIP_WAIT, this);
                }
            }
        });
    }

    /**
     * Liang-Barsky line clipping function. Adapted from Daniel White
     *
     * @param edgeLeft
     * @param edgeRight
     * @param edgeTop
     * @param edgeBottom
     * @param x0src
     * @param y0src
     * @param x1src
     * @param y1src
     * @param clip
     * @return
     * @see http://www.skytopia.com/project/articles/compsci/clipping.html
     */
     private boolean liangBarsky (double edgeLeft, double edgeRight, double edgeTop, double edgeBottom,
                       double x0src, double y0src, double x1src, double y1src, 
                       int[] clip) {
    
         // Init
         double t0 = 0.0;    
         double t1 = 1.0;
         double xdelta = x1src-x0src;
         double ydelta = y1src-y0src;
         double p = 0, q = 0,r = 0;
    
         for(int edge=0; edge<4; edge++) {
             if (edge==0) {  p = -xdelta;    q = -(edgeLeft-x0src);  }
             if (edge==1) {  p = xdelta;     q =  (edgeRight-x0src); }
             if (edge==2) {  p = -ydelta;    q = -(edgeTop-y0src);}
             if (edge==3) {  p = ydelta;     q =  (edgeBottom-y0src);   }   
             r = q/p;
             if(p==0 && q<0) return false;
    
             if(p<0) {
                 if(r>t1) return false;
                 else if(r>t0) t0=r;
             } else if(p>0) {
                 if(r<t0) return false;
                 else if(r<t1) t1=r;
             }
         }
    
         // Clip
         clip[0] = (int)(x0src + t0*xdelta);
         clip[1] = (int)(y0src + t0*ydelta);
         clip[2] = (int)(x0src + t1*xdelta);
         clip[3] = (int)(y0src + t1*ydelta);
         return true;
     }

    @Override
    protected void actionRedraw() {
        this.canvas.redraw();
    }

    @Override
    protected void eventFilterChanged(ARXResult result, ModelNodeFilter filter) {
        initialize(result, filter);
        canvas.redraw();
    }

    @Override
    protected void eventModelChanged() {
        // Empty by design
    }

    @Override
    protected void eventNodeSelected() {
        canvas.redraw();
    }

    @Override
    protected void eventResultChanged(ARXResult result) {
        if (getModel().getResult() == null) reset();
    }
}
