package rescuecore2.misc.gui;

import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.BorderFactory;
import javax.swing.Action;
import javax.swing.AbstractAction;
import javax.swing.JPopupMenu;

import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.Color;
import java.awt.Shape;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Insets;
import java.awt.BasicStroke;
import java.awt.Rectangle;
import java.awt.Point;
import java.awt.geom.Area;
import java.awt.geom.PathIterator;
import java.awt.geom.Path2D;
import java.awt.geom.Rectangle2D;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.BrokenBarrierException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Arrays;

import rescuecore2.misc.geometry.Point2D;
import rescuecore2.misc.geometry.Line2D;
import rescuecore2.misc.geometry.GeometryTools2D;
//import rescuecore2.log.Logger;

/**
   A JFrame that can be used to debug geometric shape operations. When {@link #enable enabled} this frame will block whenever a show method is called until the user clicks on a button to continue. The "step" button will cause the show method to return and leave the frame visible and activated. The "continue" button will hide and {@link #deactivate} the frame so that further calls to show will return immediately.
 */
public class ShapeDebugFrame extends JFrame {
    private static final int DISPLAY_WIDTH = 500;
    private static final int DISPLAY_HEIGHT = 500;
    private static final int LEGEND_WIDTH = 500;
    private static final int LEGEND_HEIGHT = 500;

    private static final double ZOOM_TO_OFFSET = 0.1;
    private static final double ZOOM_TO_WIDTH_FACTOR = 1.2;

    private JLabel title;
    private JButton step;
    private JButton cont;
    private ShapeViewer viewer;
    private ShapeInfoLegend legend;
    private CyclicBarrier barrier;
    private boolean enabled;
    private Collection<? extends ShapeInfo> background;
    private boolean backgroundEnabled;
    private JPopupMenu menu;
    private boolean autoZoom;

    /**
       Construct a new ShapeDebugFrame.
    */
    public ShapeDebugFrame() {
        barrier = new CyclicBarrier(2);
        viewer = new ShapeViewer();
        legend = new ShapeInfoLegend();
        step = new JButton("Step");
        cont = new JButton("Continue");
        title = new JLabel();
        add(title, BorderLayout.NORTH);
        add(viewer, BorderLayout.CENTER);
        JPanel buttons = new JPanel(new GridLayout(1, 2));
        buttons.add(step);
        buttons.add(cont);
        add(buttons, BorderLayout.SOUTH);
        add(legend, BorderLayout.EAST);
        legend.setBorder(BorderFactory.createTitledBorder("Legend"));
        viewer.setBorder(BorderFactory.createTitledBorder("Shapes"));
        step.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    try {
                        barrier.await();
                    }
                    // CHECKSTYLE:OFF:EmptyBlock
                    catch (InterruptedException ex) {
                        // Ignore
                    }
                    catch (BrokenBarrierException ex) {
                        // Ignore
                    }
                    // CHECKSTYLE:ON:EmptyBlock
                }
            });
        cont.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    try {
                        deactivate();
                        barrier.await();
                    }
                    // CHECKSTYLE:OFF:EmptyBlock
                    catch (InterruptedException ex) {
                        // Ignore
                    }
                    catch (BrokenBarrierException ex) {
                        // Ignore
                    }
                    // CHECKSTYLE:ON:EmptyBlock
                }
            });
        addWindowListener(new WindowAdapter() {
                public void windowClosing(WindowEvent e) {
                    try {
                        barrier.await();
                    }
                    // CHECKSTYLE:OFF:EmptyBlock
                    catch (InterruptedException ex) {
                        // Ignore
                    }
                    catch (BrokenBarrierException ex) {
                        // Ignore
                    }
                    // CHECKSTYLE:ON:EmptyBlock
                }
            });
        MouseAdapter m = new MouseAdapter() {
                @Override
                public void mousePressed(MouseEvent e) {
                    if (e.isPopupTrigger()) {
                        menu.show(e.getComponent(), e.getPoint().x, e.getPoint().y);
                    }
                }
                @Override
                public void mouseReleased(MouseEvent e) {
                    if (e.isPopupTrigger()) {
                        menu.show(e.getComponent(), e.getPoint().x, e.getPoint().y);
                    }
                }
                @Override
                public void mouseClicked(MouseEvent e) {
                    if (e.isPopupTrigger()) {
                        menu.show(e.getComponent(), e.getPoint().x, e.getPoint().y);
                    }
                }
            };
        addMouseListener(m);
        viewer.addMouseListener(m);
        enabled = true;
        clearBackground();
        backgroundEnabled = true;
        autoZoom = true;
        pack();
        menu = new JPopupMenu();
        menu.add(new BackgroundAction());
    }

    /**
       Set the "background" shapes. These will be drawn on every invocation of show.
       @param back The new background shapes. This should not be null.
    */
    public void setBackground(Collection<? extends ShapeInfo> back) {
        background = back;
        if (background == null) {
            clearBackground();
        }
    }

    /**
       Clear the "background" shapes.
    */
    public void clearBackground() {
        background = new ArrayList<ShapeInfo>();
    }

    /**
       Set whether the background is drawn or not.
       @param b True if the background should be drawn, false otherwise.
    */
    public void setBackgroundEnabled(boolean b) {
        backgroundEnabled = b;
    }

    /**
       Set whether autozoom is enabled.
       @param b True if autozoom should be enabled, false otherwise.
    */
    public void setAutozoomEnabled(boolean b) {
        autoZoom = b;
    }

    /**
       Show a set of ShapeInfo objects. If this frame is enabled then this method will block until the user clicks a button to continue.
       @param description A description.
       @param shapes A list of collections of ShapeInfo objects.
    */
    public void show(String description, Collection<? extends ShapeInfo>... shapes) {
        List<ShapeInfo> all = new ArrayList<ShapeInfo>();
        for (Collection<? extends ShapeInfo> next : shapes) {
            all.addAll(next);
        }
        show(description, all);
    }

    /**
       Show a set of ShapeInfo objects. If this frame is enabled then this method will block until the user clicks a button to continue.
       @param description A description.
       @param shapes An array of ShapeInfo objects.
    */
    public void show(String description, ShapeInfo... shapes) {
        show(description, Arrays.asList(shapes));
    }

    /**
       Show a set of ShapeInfo objects. If this frame is enabled then this method will block until the user clicks a button to continue.
       @param description A description.
       @param shapes A collection of ShapeInfo objects.
    */
    public void show(final String description, final Collection<ShapeInfo> shapes) {
        if (!enabled) {
            return;
        }
        final List<ShapeInfo> allShapes = new ArrayList<ShapeInfo>(shapes);
        setVisible(true);
        SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    if (description == null) {
                        title.setText("");
                    }
                    else {
                        title.setText(description);
                    }
                    legend.setShapes(allShapes);
                    viewer.setShapes(allShapes);
                    if (autoZoom) {
                        viewer.zoomTo(shapes);
                    }
                    repaint();
                }
            });
        try {
            barrier.await();
        }
        // CHECKSTYLE:OFF:EmptyBlock
        catch (InterruptedException e) {
            // Ignore
        }
        catch (BrokenBarrierException e) {
            // Ignore
        }
        // CHECKSTYLE:ON:EmptyBlock
    }

    /**
       Activate this frame. Future calls to show will block until the user clicks a button.
    */
    public void activate() {
        enabled = true;
    }

    /**
       Deactivate and hides this frame. Future calls to show will return immediately.
    */
    public void deactivate() {
        enabled = false;
        setVisible(false);
    }

    private Rectangle2D getBounds(Collection<? extends ShapeInfo>... shapes) {
        double minX = Double.POSITIVE_INFINITY;
        double minY = Double.POSITIVE_INFINITY;
        double maxX = Double.NEGATIVE_INFINITY;
        double maxY = Double.NEGATIVE_INFINITY;
        for (Collection<? extends ShapeInfo> c : shapes) {
            if (c != null) {
                for (ShapeInfo next : c) {
                    Shape bounds = next.getBoundsShape();
                    if (bounds != null) {
                        Rectangle2D rect = bounds.getBounds2D();
                        minX = Math.min(minX, rect.getMinX());
                        maxX = Math.max(maxX, rect.getMaxX());
                        minY = Math.min(minY, rect.getMinY());
                        maxY = Math.max(maxY, rect.getMaxY());
                    }
                    java.awt.geom.Point2D point = next.getBoundsPoint();
                    if (point != null) {
                        minX = Math.min(minX, point.getX());
                        maxX = Math.max(maxX, point.getX());
                        minY = Math.min(minY, point.getY());
                        maxY = Math.max(maxY, point.getY());
                    }
                }
            }
        }
        return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY);
    }

    private class ShapeViewer extends JComponent {
        private List<ShapeInfo> shapes;
        private ScreenTransform transform;
        private PanZoomListener panZoom;
        private Map<Shape, ShapeInfo> drawnShapes;

        /**
           Create a ShapeViewer.
        */
        public ShapeViewer() {
            panZoom = new PanZoomListener(this);
            drawnShapes = new HashMap<Shape, ShapeInfo>();
            shapes = new ArrayList<ShapeInfo>();
            addMouseListener(new MouseAdapter() {
                    public void mouseClicked(MouseEvent e) {
                        if (e.getButton() == MouseEvent.BUTTON1) {
                            Insets insets = getInsets();
                            Point p = new Point(e.getPoint());
                            p.translate(-insets.left, -insets.top);
                            List<ShapeInfo> s = getShapesAtPoint(p);
                            for (ShapeInfo next : s) {
                                System.out.println(next.getObject());
                            }
                        }
                    }
                });
        }

        @Override
        public void paintComponent(Graphics graphics) {
            super.paintComponent(graphics);
            drawnShapes.clear();
            if (shapes.isEmpty()) {
                return;
            }
            Insets insets = getInsets();
            int width = getWidth() - insets.left - insets.right;
            int height = getHeight() - insets.top - insets.bottom;
            transform.rescale(width, height);
            //            Logger.debug("View bounds: " + transform.getViewBounds());
            for (ShapeInfo next : shapes) {
                boolean visible = transform.isInView(next.getBoundsShape()) || transform.isInView(next.getBoundsPoint());
                if (visible) {
                    Graphics g = graphics.create(insets.left, insets.top, width, height);
                    Shape shape = next.paint((Graphics2D)g, transform);
                    if (shape != null) {
                        drawnShapes.put(shape, next);
                    }
                }
                //                else {
                    //                    Logger.debug("Pruned " + next);
                    //                    Logger.debug("Shape bounds: " + next.getBoundsShape());
                    //                    Logger.debug("Point bounds: " + next.getBoundsPoint());
                    //                }
            }
            if (backgroundEnabled) {
                for (ShapeInfo next : background) {
                    boolean visible = transform.isInView(next.getBoundsShape()) || transform.isInView(next.getBoundsPoint());
                    if (visible) {
                        Graphics g = graphics.create(insets.left, insets.top, width, height);
                        Shape shape = next.paint((Graphics2D)g, transform);
                        if (shape != null) {
                            drawnShapes.put(shape, next);
                        }
                    }
                }
            }
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(DISPLAY_WIDTH, DISPLAY_HEIGHT);
        }

        /**
           Set the list of ShapeInfo objects to draw.
           @param s The new list of ShapeInfo objects.
        */
        @SuppressWarnings("unchecked")
        public void setShapes(Collection<ShapeInfo> s) {
            shapes.clear();
            shapes.addAll(s);
            Rectangle2D bounds = ShapeDebugFrame.this.getBounds(shapes, backgroundEnabled ? background : null);
            transform = new ScreenTransform(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMaxY());
            panZoom.setScreenTransform(transform);
            repaint();
        }

        /**
           Zoom to show a set of ShapeInfo objects.
           @param zoom The set of objects to zoom to.
        */
        @SuppressWarnings("unchecked")
        public void zoomTo(Collection<ShapeInfo> zoom) {
            Rectangle2D bounds = ShapeDebugFrame.this.getBounds(zoom);
            // Increase the bounds by 10%
            double newX = bounds.getMinX() - (bounds.getWidth() * ZOOM_TO_OFFSET);
            double newY = bounds.getMinY() - (bounds.getHeight() * ZOOM_TO_OFFSET);
            double newWidth = bounds.getWidth() * ZOOM_TO_WIDTH_FACTOR;
            double newHeight = bounds.getHeight() * ZOOM_TO_WIDTH_FACTOR;
            bounds.setRect(newX, newY, newWidth, newHeight);
            transform.show(bounds);
            repaint();
        }

        private List<ShapeInfo> getShapesAtPoint(Point p) {
            List<ShapeInfo> result = new ArrayList<ShapeInfo>();
            for (Map.Entry<Shape, ShapeInfo> next : drawnShapes.entrySet()) {
                Shape shape = next.getKey();
                if (shape.contains(p)) {
                    result.add(next.getValue());
                }
            }
            return result;
        }
    }

    /**
       The legend for the debug frame.
    */
    private class ShapeInfoLegend extends JComponent {
        private static final int ROW_OFFSET = 5;
        private static final int X_INDENT = 5;
        private static final int ENTRY_WIDTH = 50;
        private static final int ENTRY_HEIGHT = 9;

        private List<ShapeInfo> shapes;

        ShapeInfoLegend() {
            shapes = new ArrayList<ShapeInfo>();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(LEGEND_WIDTH, LEGEND_HEIGHT);
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            if (shapes.isEmpty()) {
                return;
            }
            Set<String> seen = new HashSet<String>();
            FontMetrics metrics = g.getFontMetrics();
            int height = metrics.getHeight();
            int y = getInsets().top;
            int x = getInsets().left + X_INDENT;
            if (backgroundEnabled) {
                for (ShapeInfo next : background) {
                    String name = next.getName();
                    if (name == null || "".equals(name)) {
                        continue;
                    }
                    if (seen.contains(name)) {
                        continue;
                    }
                    seen.add(name);
                    next.paintLegend((Graphics2D)g.create(x, y + (height / 2) - (ENTRY_HEIGHT / 2), ENTRY_WIDTH, ENTRY_HEIGHT), ENTRY_WIDTH, ENTRY_HEIGHT);
                    g.setColor(Color.black);
                    g.drawString(next.getName(), x + ENTRY_WIDTH + X_INDENT, y + metrics.getAscent());
                    y += height + ROW_OFFSET;
                }
            }
            for (ShapeInfo next : shapes) {
                String name = next.getName();
                if (name == null || "".equals(name)) {
                    continue;
                }
                if (seen.contains(name)) {
                    continue;
                }
                seen.add(name);
                next.paintLegend((Graphics2D)g.create(x, y + (height / 2) - (ENTRY_HEIGHT / 2), ENTRY_WIDTH, ENTRY_HEIGHT), ENTRY_WIDTH, ENTRY_HEIGHT);
                g.setColor(Color.black);
                g.drawString(next.getName(), x + ENTRY_WIDTH + X_INDENT, y + metrics.getAscent());
                y += height + ROW_OFFSET;
            }
        }

        /**
           Set the list of shapes.
           @param s The new list of shapes.
        */
        public void setShapes(Collection<ShapeInfo> s) {
            shapes.clear();
            shapes.addAll(s);
            repaint();
        }
    }

    /**
       This class captures information about a shape that should be displayed on-screen.
    */
    public abstract static class ShapeInfo {
        /** The name of the shape. */
        protected String name;
        /** The object this shape represents. */
        private Object object;

        /**
           Construct a new ShapeInfo object.
           @param object The object this shape represents.
           @param name The name of the shape.
        */
        protected ShapeInfo(Object object, String name)  {
            this.object = object;
            this.name = name;
        }

        /**
           Paint this ShapeInfo on a Graphics2D object.
           @param g The Graphics2D to draw on.
           @param transform The current screen transform.
           @return A shape for mouseover detection.
        */
        public abstract Shape paint(Graphics2D g, ScreenTransform transform);

        /**
           Paint this ShapeInfo on a the legend.
           @param g The Graphics2D to draw on.
           @param width The available width.
           @param height The available height.
        */
        public abstract void paintLegend(Graphics2D g, int width, int height);

        /**
           Get the object this shape represents.
           @return The object.
        */
        public Object getObject() {
            return object;
        }

        /**
           Get the name of this shape info.
           @return The name.
        */
        public String getName() {
            return name;
        }

        /**
           Get the bounding shape of this shape.
           @return The bounding shape or null if this shape represents a point.
        */
        public abstract Shape getBoundsShape();

        /**
           Get the point representing this shape.
           @return The shape point or null if this shape does not represent a point.
        */
        public abstract java.awt.geom.Point2D getBoundsPoint();
    }

    /**
       A ShapeInfo that encapsulates an awt Shape.
    */
    public static class AWTShapeInfo extends ShapeInfo {
        private Shape shape;
        private boolean fill;
        private Color colour;
        private Rectangle2D bounds;

        /**
           Construct a new AWTShapeInfo object.
           @param shape The shape to display.
           @param name The name of the shape.
           @param colour The colour of the shape.
           @param fill Whether to fill the shape.
        */
        public AWTShapeInfo(Shape shape, String name, Color colour, boolean fill) {
            super(shape, name);
            this.shape = shape;
            this.fill = fill;
            this.colour = colour;
            if (shape != null) {
                bounds = shape.getBounds2D();
            }
        }

        @Override
        public Shape paint(Graphics2D g, ScreenTransform transform) {
            if (shape == null || (shape instanceof Area && ((Area)shape).isEmpty())) {
                return null;
            }
            Path2D path = new Path2D.Double();
            PathIterator pi = shape.getPathIterator(null);
            // CHECKSTYLE:OFF:MagicNumber
            double[] d = new double[6];
            while (!pi.isDone()) {
                int type = pi.currentSegment(d);
                switch (type) {
                case PathIterator.SEG_MOVETO:
                    path.moveTo(transform.xToScreen(d[0]), transform.yToScreen(d[1]));
                    break;
                case PathIterator.SEG_LINETO:
                    path.lineTo(transform.xToScreen(d[0]), transform.yToScreen(d[1]));
                    break;
                case PathIterator.SEG_CLOSE:
                    path.closePath();
                    break;
                case PathIterator.SEG_QUADTO:
                    path.quadTo(transform.xToScreen(d[0]), transform.yToScreen(d[1]), transform.xToScreen(d[2]), transform.yToScreen(d[3]));
                    break;
                case PathIterator.SEG_CUBICTO:
                    path.curveTo(transform.xToScreen(d[0]), transform.yToScreen(d[1]), transform.xToScreen(d[2]), transform.yToScreen(d[3]), transform.xToScreen(d[4]), transform.yToScreen(d[5]));
                    break;
                default:
                    throw new RuntimeException("Unexpected PathIterator constant: " + type);
                }
                pi.next();
            }
            // CHECKSTYLE:ON:MagicNumber
            g.setColor(colour);
            if (fill) {
                g.fill(path);
            }
            else {
                g.draw(path);
            }
            return path.createTransformedShape(null);
        }

        @Override
        public void paintLegend(Graphics2D g, int width, int height) {
            if (shape == null) {
                return;
            }
            g.setColor(colour);
            if (fill) {
                g.fillRect(0, 0, width, height);
            }
            else {
                g.drawRect(0, 0, width - 1, height - 1);
            }
        }

        @Override
        public Rectangle2D getBoundsShape() {
            return bounds;
        }

        @Override
        public java.awt.geom.Point2D getBoundsPoint() {
            return null;
        }
    }

    /**
       A ShapeInfo that encapsulates a Point2D.
    */
    public static class Point2DShapeInfo extends ShapeInfo {
        private static final int SIZE = 3;

        private Point2D point;
        private java.awt.geom.Point2D boundsPoint;
        private boolean square;
        private Color colour;

        /**
           Construct a new Point2DShapeInfo object.
           @param point The point to display.
           @param name The name of the point.
           @param colour The colour of the point.
           @param square Whether to draw as a square or a cross. If false then a cross will be drawn.
        */
        public Point2DShapeInfo(Point2D point, String name, Color colour, boolean square) {
            super(point, name);
            this.point = point;
            this.square = square;
            this.colour = colour;
            if (point != null) {
                boundsPoint = new java.awt.geom.Point2D.Double(point.getX(), point.getY());
            }
        }

        @Override
        public Shape paint(Graphics2D g, ScreenTransform transform) {
            if (point == null) {
                return null;
            }
            int x = transform.xToScreen(point.getX());
            int y = transform.yToScreen(point.getY());
            g.setColor(colour);
            if (square) {
                g.fillRect(x - SIZE, y - SIZE, SIZE * 2, SIZE * 2);
            }
            else {
                g.drawLine(x - SIZE, y - SIZE, x + SIZE, y + SIZE);
                g.drawLine(x - SIZE, y + SIZE, x + SIZE, y - SIZE);
            }
            //            Logger.debug("Painting point " + name + " (" + point + ") at " + x + ", " + y);
            return new Rectangle(x - SIZE, y - SIZE, SIZE * 2, SIZE * 2);
        }

        @Override
        public void paintLegend(Graphics2D g, int width, int height) {
            if (point == null) {
                return;
            }
            g.setColor(colour);
            int x = (width / 2);
            int y = (height / 2);
            if (square) {
                g.fillRect(x - SIZE, y - SIZE, SIZE * 2, SIZE * 2);
            }
            else {
                g.drawLine(x - SIZE, y - SIZE, x + SIZE, y + SIZE);
                g.drawLine(x - SIZE, y + SIZE, x + SIZE, y - SIZE);
            }
        }

        @Override
        public Shape getBoundsShape() {
            return null;
        }

        @Override
        public java.awt.geom.Point2D getBoundsPoint() {
            return boundsPoint;
        }
    }

    /**
       A ShapeInfo that encapsulates a Line2D.
    */
    public static class Line2DShapeInfo extends ShapeInfo {
        private static final int SIZE = 2;
        private static final BasicStroke THICK_STROKE = new BasicStroke(SIZE * 3, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);
        private static final BasicStroke THIN_STROKE = new BasicStroke(SIZE, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL);

        private Collection<Line2D> lines;
        private Shape bounds;
        private boolean arrow;
        private boolean thick;
        private Color colour;

        /**
           Construct a new Line2DShapeInfo object.
           @param line The line to display.
           @param name The name of the line.
           @param colour The colour of the line.
           @param thick Whether to draw the line with a thick stroke.
           @param arrow Whether to draw an arrow showing the direction of the line.
        */
        public Line2DShapeInfo(Line2D line, String name, Color colour, boolean thick, boolean arrow) {
            this(Collections.singleton(line), name, colour, thick, arrow);
        }

        /**
           Construct a new Line2DShapeInfo object.
           @param lines The lines to display.
           @param name The name of the line.
           @param colour The colour of the line.
           @param thick Whether to draw the line with a thick stroke.
           @param arrow Whether to draw an arrow showing the direction of the line.
        */
        public Line2DShapeInfo(Collection<Line2D> lines, String name, Color colour, boolean thick, boolean arrow) {
            super(lines, name);
            this.lines = lines;
            this.arrow = arrow;
            this.thick = thick;
            this.colour = colour;
            if (lines.isEmpty()) {
                return;
            }
            if (lines.size() == 1) {
                Line2D l = lines.iterator().next();
                bounds = new java.awt.geom.Line2D.Double(l.getOrigin().getX(), l.getOrigin().getY(), l.getEndPoint().getX(), l.getEndPoint().getY());
            }
            else {
                double xMin = Double.POSITIVE_INFINITY;
                double yMin = Double.POSITIVE_INFINITY;
                double xMax = Double.NEGATIVE_INFINITY;
                double yMax = Double.NEGATIVE_INFINITY;
                for (Line2D line : lines) {
                    xMin = Math.min(xMin, line.getOrigin().getX());
                    xMax = Math.max(xMax, line.getOrigin().getX());
                    xMin = Math.min(xMin, line.getEndPoint().getX());
                    xMax = Math.max(xMax, line.getEndPoint().getX());
                    yMin = Math.min(yMin, line.getOrigin().getY());
                    yMax = Math.max(yMax, line.getOrigin().getY());
                    yMin = Math.min(yMin, line.getEndPoint().getY());
                    yMax = Math.max(yMax, line.getEndPoint().getY());
                }
                double xRange = xMax - xMin;
                double yRange = yMax - yMin;
                bounds = new Rectangle2D.Double(xMin, yMin, xMax - xMin, yMax - yMin);
                if (GeometryTools2D.nearlyZero(xRange) || GeometryTools2D.nearlyZero(yRange)) {
                    bounds = new java.awt.geom.Line2D.Double(xMin, yMin, xMax, yMax);
                }
            }
        }

        @Override
        public Shape paint(Graphics2D g, ScreenTransform transform) {
            if (lines.isEmpty()) {
                return null;
            }
            if (thick) {
                g.setStroke(THICK_STROKE);
            }
            else {
                g.setStroke(THIN_STROKE);
            }
            g.setColor(colour);
            Path2D result = new Path2D.Double();
            for (Line2D line : lines) {
                Point2D start = line.getOrigin();
                Point2D end = line.getEndPoint();
                int x1 = transform.xToScreen(start.getX());
                int y1 = transform.yToScreen(start.getY());
                int x2 = transform.xToScreen(end.getX());
                int y2 = transform.yToScreen(end.getY());
                g.drawLine(x1, y1, x2, y2);
                if (arrow) {
                    DrawingTools.drawArrowHeads(x1, y1, x2, y2, g);
                }
                result.moveTo(x1, y1);
                result.lineTo(x2, y2);
                //                Logger.debug("Painting line " + name + " (" + line + ") from " + x1 + ", " + y1 + " -> " + x2 + ", " + y2);
            }
            return g.getStroke().createStrokedShape(result);
        }

        @Override
        public void paintLegend(Graphics2D g, int width, int height) {
            if (thick) {
                g.setStroke(THICK_STROKE);
            }
            else {
                g.setStroke(THIN_STROKE);
            }
            g.setColor(colour);
            g.drawLine(0, height / 2, width, height / 2);
            if (arrow) {
                DrawingTools.drawArrowHeads(0, height / 2, width, height / 2, g);
            }
        }

        @Override
        public Shape getBoundsShape() {
            return bounds;
        }

        @Override
        public java.awt.geom.Point2D getBoundsPoint() {
            return null;
        }
    }

    private class BackgroundAction extends AbstractAction {
        public BackgroundAction() {
            super(backgroundEnabled ? "Hide background" : "Show background");
            putValue(Action.SELECTED_KEY, Boolean.valueOf(backgroundEnabled));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            boolean selected = ((Boolean)getValue(Action.SELECTED_KEY)).booleanValue();
            setBackgroundEnabled(!selected);
            putValue(Action.SELECTED_KEY, Boolean.valueOf(backgroundEnabled));
            putValue(Action.NAME, backgroundEnabled ? "Hide background" : "Show background");
            ShapeDebugFrame.this.repaint();
        }
    }
}