/*
 * @(#)StandardDrawingView.java 5.1
 *
 */

package CH.ifa.draw.standard;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.PrintGraphics;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
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.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.io.File;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JViewport;

import bibliothek.gui.DockUI;
import bibliothek.gui.dock.util.laf.LookAndFeelColors;

import CH.ifa.draw.application.AbstractFileDragDropListener;
import CH.ifa.draw.figures.CompositeAttributeFigure;
import CH.ifa.draw.figures.ImageFigure;
import CH.ifa.draw.figures.ImageFigureCreationTool;
import CH.ifa.draw.framework.ChildFigure;
import CH.ifa.draw.util.ContextGraphics;
import CH.ifa.draw.util.Geom;
import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.storables.ontology.DrawingChangeEvent;
import de.renew.draw.storables.ontology.DrawingChangeListener;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureChangeEvent;
import de.renew.draw.storables.ontology.FigureChangeListener;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.draw.storables.ontology.FigureWithDependencies;
import de.renew.draw.ui.ontology.AbstractCommand;
import de.renew.draw.ui.ontology.DrawingEditor;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.draw.ui.ontology.FigureHandle;
import de.renew.draw.ui.ontology.FigureSelection;
import de.renew.draw.ui.ontology.Painter;
import de.renew.draw.ui.ontology.PointConstrainer;
import de.renew.draw.ui.ontology.Tool;


/**
 * The standard implementation of DrawingView.
 * @see DrawingView
 * @see Painter
 * @see Tool
 */
public class StandardDrawingView extends JPanel implements DrawingView, FigureChangeListener,
    MouseListener, MouseWheelListener, MouseMotionListener, KeyListener
{
    /*
     * Serialization support. In JavaDraw only the Drawing is serialized.
     * However, for beans support StandardDrawingView supports
     * serialization
     */
    @Serial
    private static final long serialVersionUID = -3878153366174603336L;
    private final int _drawingViewSerializedDataVersion = 1;

    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(StandardDrawingView.class);

    /**
     * The DrawingEditor of the view.
     * @see #tool
     */
    transient private DrawingEditor _editor;

    /**
     * The shown drawing.
     */
    private Drawing _drawing;

    /**
     * the accumulated damaged area
     */
    private transient Rectangle _damage = null;

    /**
     * The list of currently selected figures.
     */
    private transient Vector<Figure> _selection;

    /**
     * Caches the shown selection handles.
     * Do not access (not even for reading) this variable
     * directly, always use {@link #selectionHandles} and
     * {@link #selectionInvalidateHandles}!
     **/
    transient private Vector<FigureHandle> _selectionHandles;

    /**
     * This object's monitor is used to synchronize the update of
     * the {@link #_selectionHandles} cache.
     **/
    transient private Object _selectionHandlesLock = new Object();

    /**
     * The preferred size of the view
     */
    private final Dimension _viewSize;

    /**
     * The position of the last mouse click
     * inside the view.
     */
    private Point _lastClick;

    /**
     * A vector of optional backgrounds. The vector maintains
     * a list a view painters that are drawn before the contents,
     * that is in the background.
     */
    private Vector<Painter> _backgrounds = null;

    /**
     * A vector of optional foregrounds. The vector maintains
     * a list a view painters that are drawn after the contents,
     * that is in the foreground.
     */
    private Vector<Painter> _foregrounds = null;

    /**
     * The update strategy used to repair the view.
     */
    private Painter _updateStrategy;

    /**
     * The grid used to constrain points for snap to
     * grid functionality.
     */
    private PointConstrainer _constrainer;

    /**
     * The monitor of this object is used to synchronize
     * the merging of overlapping repaint events.
     **/
    private transient Object _repaintLock = new Object();

    /**
     * The factor by which the drawing is zoomed.
     */
    private double _scaleFactor = 1.0;
    private final double _maxScaling = 3.0;
    private final double _minScaling = 0.2;

    /**
     * The affine transformation currently applied to the graphics of this view.
     */
    private AffineTransform _affinetransform;

    /**
     * Constructs the view.
     */
    public StandardDrawingView(DrawingEditor editor, int width, int height) {
        _editor = editor;
        _viewSize = new Dimension(width, height);
        _lastClick = new Point(0, 0);
        _constrainer = null;
        _selection = new Vector<>();
        setDisplayUpdate(new SimpleUpdateStrategy());
        setBackground(Color.lightGray);
        setOpaque(true);
        setLayout(null);
        _affinetransform = AffineTransform.getScaleInstance(1.0, 1.0);

        DropTargetListener dragDropListener = new AbstractFileDragDropListener() {
            @Override
            protected void handleFilesDrawing(final File[] files, Point loc) {
                for (File file : files) {
                    if (!file.isDirectory()) {
                        _editor.openOrLoadDrawing(file, StandardDrawingView.this);
                    }
                }
            }

            @Override
            protected void handleFilesImage(File[] files, Point loc) {
                if (files.length >= 1) {
                    Vector<Figure> imagesToAdd = new Vector<>();
                    for (File file : files) {
                        Image image = ImageFigureCreationTool
                            .createImage(file.getAbsolutePath(), StandardDrawingView.this);

                        Point p = constrainPoint(new Point(loc.x, loc.y));
                        inverseTransformPoint(p);
                        ImageFigure imageFigure = new ImageFigure(image, file.getAbsolutePath(), p);
                        Rectangle displayBox = imageFigure.displayBox();
                        imageFigure.moveBy(-(displayBox.width / 2), -(displayBox.height / 2));
                        imagesToAdd.add(imageFigure);
                    }
                    StandardDrawingView.this.addAll(imagesToAdd);
                    StandardDrawingView.this.checkDamage();
                }
            }
        };
        new DropTarget(this, dragDropListener);
    }

    /**
     * Sets the view's editor.
     * @param editor editor to be set
     * @deprecated This method is currently not used and will be removed in a later version.
     */
    @Deprecated
    public void setEditor(DrawingEditor editor) {
        _editor = editor;
    }

    /**
     * Gets the current tool.
     * @return current tool
     */
    public Tool tool() {
        return _editor.tool();
    }

    /**
     * Gets the drawing.
     * @return current drawing
     */
    @Override
    public Drawing drawing() {
        return _drawing;
    }

    /**
     * Sets and installs another drawing in the view.
     * @param d drawing to be set/installed
     */
    @Override
    public void setDrawing(Drawing d) {
        if (_drawing != null) {
            clearSelection();
            _drawing.removeDrawingChangeListener(this);
        }

        _drawing = d;
        _drawing.addDrawingChangeListener(this);
        checkMinimumSize(null);
        repaint();
    }

    /**
     * Sets the drawing of this {@link StandardDrawingView} to {@link NullDrawing#INSTANCE}.
     */
    public void setNullDrawing() {
        setDrawing(NullDrawing.INSTANCE);
    }

    /**
     * Gets the editor.
     * @return editor
     */
    @Override
    public DrawingEditor editor() {
        return _editor;
    }

    /**
     * Adds a figure to the drawing.
     * @param figure figure to added
     * @return the added figure.
     */
    @Override
    public Figure add(Figure figure) {
        return drawing().add(figure);
    }

    /**
     * Removes a figure from the drawing.
     * @param figure vector of figures to be removed
     * @return the removed figure
     */
    @Override
    public Figure remove(Figure figure) {
        return drawing().remove(figure);
    }

    /**
     * Adds a vector of figures to the drawing.
     * @param figures vector of figures to be added
     */
    @Override
    public void addAll(Vector<Figure> figures) {
        FigureEnumeration k = new FigureEnumerator(figures);
        while (k.hasMoreElements()) {
            add(k.nextFigure());
        }
    }

    /**
     * Removes a vector of figures from the drawing.
     * @param figures vector of figures to be removed
     */
    public void removeAll(Vector<Figure> figures) {
        FigureEnumeration k = new FigureEnumerator((Vector<Figure>) figures.clone());
        while (k.hasMoreElements()) {
            remove(k.nextFigure());
        }
    }

    /**
     * Gets the minimum dimension of the drawing.
     * @return minimum size of this drawing
     */
    public Dimension getMinimumSize() {
        Dimension result = _viewSize;

        if (_scaleFactor > 1.0 && _affinetransform != null) {
            Rectangle r = _affinetransform
                .createTransformedShape(new Rectangle(_viewSize.width, _viewSize.height))
                .getBounds();
            result = r.getSize();
        }

        return result;
    }

    /**
     * Gets the preferred dimension of the drawing.
     */
    @Override
    public Dimension getPreferredSize() {
        return getMinimumSize();
    }

    /**
     * Sets the current display update strategy.
     * @see Painter
     * @param updateStrategy update Strategy to be set
     * @deprecated This method is only for internal usage and will later be made private.
     */
    @Deprecated
    public void setDisplayUpdate(Painter updateStrategy) {
        _updateStrategy = updateStrategy;
    }

    /**
     * Gets the currently selected figures.
     * @return a vector with the selected figures. The vector
     * is a copy of the current selection.
     */
    @Override
    public Vector<Figure> selection() {
        //protect the vector with the current selection
        return new Vector<>(_selection);
    }

    /**
     * Gets an enumeration over the currently selected figures.
     */
    @Override
    public FigureEnumeration selectionElements() {
        // protect the vector with the current selection
        return new FigureEnumerator(selection());
    }

    /**
     * Sorts the given vector of figures into Z-order.
     * @param selection figures included in given vector
     * @return a vector with the ordered figures.
     */
    public Vector<Figure> inZOrder(Vector<Figure> selection) {
        Vector<Figure> result = new Vector<>(selection.size());
        FigureEnumeration figures = drawing().figures();

        while (figures.hasMoreElements()) {
            Figure f = figures.nextFigure();
            if (selection.contains(f)) {
                result.addElement(f);
            }
        }
        return result;
    }

    /**
     * Gets the currently selected figures in Z order.
     * @see #selection
     * @return a vector with the selected figures. The vector
     * is a copy of the current selection.
     */
    @Override
    public Vector<Figure> selectionZOrdered() {
        return inZOrder(_selection);
    }

    /**
     * Gets the number of selected figures.
     */
    @Override
    public int selectionCount() {
        return _selection.size();
    }

    /**
     * Adds a figure to the current selection.
     */
    @Override
    public void addToSelection(Figure figure) {
        if (addToSelectionInternal(figure)) {
            selectionChanged();
        }
    }

    /**
     * Adds a figure to the current selection.
     * Is used by addToSelection, addToSelectionAll.
     * Does not send selectionChanged messages.
     * @return true, if selection is changed,
     *         false, otherwise.
     */
    protected boolean addToSelectionInternal(Figure figure) {
        if (!_selection.contains(figure)) {
            if (figure.isSelectable()) {
                _selection.addElement(figure);
                figure.addFigureChangeListener(this);
                selectionInvalidateHandles();
                figure.invalidate();

                return true;
            }

            //else if (figure instanceof CompositeFigure) {
        }
        return false;
    }

    /**
     * Adds a vector of figures to the current selection.
     */
    @Override
    public void addToSelectionAll(Vector<Figure> figures) {
        addToSelectionAll(new FigureEnumerator(figures));
    }

    @Override
    public void addToSelectionAll(FigureEnumeration figures) {
        boolean changed = false;
        while (figures.hasMoreElements()) {
            changed = changed | addToSelectionInternal(figures.nextFigure());
        }
        if (changed) {
            selectionChanged();
        }
    }

    /**
     * Removes a figure from the selection.
     * @param figure figure to be removed from selection
     * @deprecated This method is only for internal usage and will later be made private.
     */
    @Deprecated
    public void removeFromSelection(Figure figure) {
        if (removeFromSelectionInternal(figure)) {
            selectionChanged();
        }
    }

    /**
     * Removes a figure from the selection.
     * Is used by removeFromSelection, removeFromSelectionAll.
     * Does not send selectionChanged messages.
     * @return true, if selection is changed,
     *         false, otherwise.
     */
    protected boolean removeFromSelectionInternal(Figure figure) {
        if (_selection.contains(figure)) {
            _selection.removeElement(figure);
            figure.removeFigureChangeListener(this);
            selectionInvalidateHandles();
            figure.invalidate();
            return true;
        }
        return false;
    }

    /**
     * Removes a vector of figures from the current selection.
     */
    @Override
    public void removeFromSelectionAll(Vector<Figure> figures) {
        removeFromSelectionAll(new FigureEnumerator(figures));
    }

    /**
     * Removes an enumeration of figures from the current selection.
     * @param figures figures to be removed
     * @deprecated This method is only for internal usage and will later be made private.
     */
    @Deprecated
    public void removeFromSelectionAll(FigureEnumeration figures) {
        boolean changed = false;
        while (figures.hasMoreElements()) {
            changed = changed | removeFromSelectionInternal(figures.nextFigure());
        }
        if (changed) {
            selectionChanged();
        }
    }

    /**
     * If a figure isn't selected it is added to the selection.
     * Otherwise, it is removed from the selection.
     * @param figure figure added if selected, otherwise removed
     */
    @Override
    public void toggleSelection(Figure figure) {
        if (toggleSelectionInternal(figure)) {
            selectionChanged();
        }
    }

    /**
     * If a figure isn't selected it is added to the selection.
     * Otherwise, it is removed from the selection.
     * Is used by toggleSelection, toggleSelectionAll.
     * Does not send selectionChanged messages.
     * @param figure figure added if selected, otherwise removed
     * @return true, if selection is changed,
     *         false, otherwise.
     * @deprecated  This method is only for internal usage and will later be made private.
     */
    @Deprecated
    public boolean toggleSelectionInternal(Figure figure) {
        if (_selection.contains(figure)) {
            return removeFromSelectionInternal(figure);
        } else {
            return addToSelectionInternal(figure);
        }
    }

    /**
     * Toggles a vector of figures.
     * If a figure isn't selected it is added to the selection.
     * Otherwise, it is removed from the selection.
     */
    @Override
    public void toggleSelectionAll(Vector<Figure> figures) {
        toggleSelectionAll(new FigureEnumerator(figures));
    }

    /**
     * Toggles an enumeration of figures.
     * If a figure isn't selected it is added to the selection.
     * Otherwise, it is removed from the selection.
     * @param figures figures to be toggled
    *  @deprecated This method is only for internal usage and will later be made private.
     */
    @Deprecated
    public void toggleSelectionAll(FigureEnumeration figures) {
        boolean changed = false;
        while (figures.hasMoreElements()) {
            changed = changed | toggleSelectionInternal(figures.nextFigure());
        }
        if (changed) {
            selectionChanged();
        }
    }

    /**
     * Clears the current selection.
     */
    @Override
    public void clearSelection() {
        FigureEnumeration k = selectionElements();
        while (k.hasMoreElements()) {
            Figure fig = k.nextFigure();
            fig.removeFigureChangeListener(this);
            fig.invalidate();
        }
        _selection = new Vector<>();
        selectionInvalidateHandles();
        selectionChanged();
    }

    @Override
    public void figureInvalidated(FigureChangeEvent e) {}

    @Override
    public void figureChanged(FigureChangeEvent e) {}

    @Override
    public void figureRemoved(FigureChangeEvent e) {
        removeFromSelection(e.getFigure());
    }

    @Override
    public void figureRequestRemove(FigureChangeEvent e) {}

    @Override
    public void figureRequestUpdate(FigureChangeEvent e) {}

    /**
     * Invalidates the handles of the current selection.
     * This means that the cached set of handles will be
     * re-calculated next time the selection's handles are
     * queried.
     */
    @Override
    public void selectionInvalidateHandles() {
        synchronized (_selectionHandlesLock) {
            _selectionHandles = null;
        }
    }

    @Override
    public void figureHandlesChanged(FigureChangeEvent e) {
        selectionInvalidateHandles();
    }

    /**
     * Gets an enumeration of the currently active handles.
     */
    private Enumeration<FigureHandle> selectionHandles() {
        synchronized (_selectionHandlesLock) {
            if (_selectionHandles == null) {
                _selectionHandles = new Vector<>();
                FigureEnumeration k = selectionElements();
                while (k.hasMoreElements()) {
                    Figure figure = k.nextFigure();
                    Enumeration<FigureHandle> kk = figure.handles().elements();
                    while (kk.hasMoreElements()) {
                        _selectionHandles.addElement(kk.nextElement());
                    }
                }
            }
            return _selectionHandles.elements();
        }
    }

    private static void tryAdd(Vector<Figure> vec, Figure obj) {
        if (obj != null && !vec.contains(obj)) {
            vec.addElement(obj);
        }
    }

    /**
     * Include into a vector of figures all those figures that are
     * referenced by the figures, also indirectly.
     */
    public static Vector<Figure> expandFigureVector(Vector<Figure> orgFigures) {
        // Clone the vector so that the existing data is not corrupted.
        Vector<Figure> figures = new Vector<>(orgFigures);

        // Add neighbors of all elements of the vector.
        // Newly added elements will be processed the same way at the end.
        // This looks like a for-loop, but it behaves like a while-loop
        // because of the concurrent modification of figures.size()
        for (int i = 0; i < figures.size(); i++) {
            Figure figure = figures.elementAt(i);
            if (figure instanceof FigureWithDependencies df) {
                FigureEnumeration relatedFigures = df.getFiguresWithDependencies();
                while (relatedFigures.hasMoreElements()) {
                    tryAdd(figures, relatedFigures.nextFigure());
                }
            }
        }
        return figures;
    }

    /**
     * Gets the current selection as a FigureSelection. A FigureSelection
     * can be cut, copied, pasted.
     */
    @Override
    public FigureSelection getFigureSelection() {
        return new CH.ifa.draw.framework.FigureSelection(inZOrder(expandFigureVector(_selection)));
    }

    /**
     * Finds a handle at the given coordinates.
     * @return the hit handle, null if no handle is found.
     */
    @Override
    public FigureHandle findHandle(int x, int y) {
        FigureHandle handle;

        Enumeration<FigureHandle> k = selectionHandles();
        while (k.hasMoreElements()) {
            handle = k.nextElement();
            if (handle.containsPoint(x, y)) {
                return handle;
            }
        }
        return null;
    }

    /**
     * Informs that the current selection changed.
     * By default, this event is forwarded to the
     * drawing editor.
     */
    protected void selectionChanged() {
        _editor.selectionChanged(this);
    }

    /**
     * Gets the position of the last click inside the view.
     */
    @Override
    public Point lastClick() {
        return _lastClick;
    }

    @Override
    public Point getCurrentMousePosition() {
        return this.getMousePosition();
    }

    /**
     * Sets the grid spacing that is used to constrain points.
     */
    @Override
    public void setConstrainer(PointConstrainer c) {
        _constrainer = c;
    }

    /**
     * Gets the current constrainer.
     */
    @Override
    public PointConstrainer getConstrainer() {
        return _constrainer;
    }

    /**
     * Constrains a point to the current grid.
     */
    protected Point constrainPoint(Point p) {
        // constrain to view size
        Dimension size = getSize();

        p.x = Geom.range(1, size.width, p.x);
        p.y = Geom.range(1, size.height, p.y);

        if (_constrainer != null) {
            return _constrainer.constrainPoint(p);
        }
        return p;
    }

    /**
     * Handles mouse down events. The event is delegated to the
     * currently active tool.
     */
    @Override
    public void mousePressed(MouseEvent e) {
        boolean rightClick = (e.getModifiersEx()
            & (InputEvent.BUTTON2_DOWN_MASK | InputEvent.BUTTON3_DOWN_MASK)) != 0;
        if (tool() != editor().defaultTool() && rightClick && e.getClickCount() == 1) {
            editor().setStickyTools(false);
            tool().cancel();
        } else {
            requestFocus(); // JDK1.1
            Point p = constrainPoint(new Point(e.getX(), e.getY()));

            inverseTransformPoint(p);
            _lastClick = new Point(e.getX(), e.getY());

            tool().mouseDown(e, p.x, p.y);
            checkDamage();
        }
    }

    /**
     * Handles mouse drag events. The event is delegated to the
     * currently active tool.
     */
    @Override
    public void mouseDragged(MouseEvent e) {
        Point p = constrainPoint(new Point(e.getX(), e.getY()));
        inverseTransformPoint(p);
        tool().mouseDrag(e, p.x, p.y);

        checkDamage();
    }

    /**
     * Handles mouse move events. The event is delegated to the
     * currently active tool.
     */
    @Override
    public void mouseMoved(MouseEvent e) {
        Point p = new Point(e.getX(), e.getY());
        inverseTransformPoint(p);
        tool().mouseMove(e, p.x, p.y);
        checkDamage();
    }

    /**
     * Handles mouse up events. The event is delegated to the
     * currently active tool.
     */
    @Override
    public void mouseReleased(MouseEvent e) {
        Point p = constrainPoint(new Point(e.getX(), e.getY()));

        inverseTransformPoint(p);
        tool().mouseUp(e, p.x, p.y);

        checkDamage();
    }

    /**
     * Handles mouse wheel events. Used for setting the zooming factor.
     * @param e
     */
    @Override
    public void mouseWheelMoved(MouseWheelEvent e) {
        Point p = e.getPoint();

        if (e.getWheelRotation() > 0) {
            zoomOutFromPoint(p);
        } else {
            zoomInOnPoint(p);
        }
    }

    /**
     * Zoom in on a point relative to the viewport.
     *
     * @param p Point relative to the viewport
     */
    public void zoomInOnPoint(Point p) {
        double factor = Math.min(_scaleFactor + 0.2, _maxScaling);
        BigDecimal bd = new BigDecimal(factor).setScale(1, RoundingMode.HALF_UP);
        zoomOnPoint(bd.doubleValue(), p);
    }

    /**
     * Zoom out from a point relative to the viewport.
     *
     * @param p Point relative to the viewport
     */
    public void zoomOutFromPoint(Point p) {
        double factor = Math.max(_scaleFactor - 0.2, _minScaling);
        BigDecimal bd = new BigDecimal(factor).setScale(1, RoundingMode.HALF_UP);
        zoomOnPoint(bd.doubleValue(), p);
    }

    /**
     * Zoom towards or away from a specified point (relative to the viewport) by the given absolute scaling factor.
     *
     * @param factor Absolute scaling factor
     * @param p Point relative to the viewport
     */
    public void zoomOnPoint(double factor, Point p) {
        JViewport viewport = (JViewport) this.getParent();
        Point portPos = viewport.getViewPosition();

        // First we get the absolute point
        Point p2 = new Point(p.x + portPos.x, p.y + portPos.y);

        // Transform it back into the basic coordinate system with the old scaling factor
        // Here we assume there is no other affine transformation done to the graphics
        inverseTransformPoint(p2);

        // update the scaling factor
        changeScalingFactor(factor);

        // Transform with the new scaling factor
        _affinetransform.transform(p2, p2);

        // Translate the viewport coordinates by the difference to the specified point
        // If the viewport would break upper or left boundaries, set to 0
        int xViewport = Math.max(p2.x - p.x, 0);
        int yViewport = Math.max(p2.y - p.y, 0);
        portPos = new Point(xViewport, yViewport);
        viewport.setViewPosition(portPos);

        // Force redraw of the whole graphic
        drawingInvalidated(new CH.ifa.draw.framework.DrawingChangeEvent(_drawing, getBounds()));
        checkDamage();
        viewport.repaint();
    }

    /**
     * Zoom in until a given rectangle (selected area in the viewport) fits exactly into the viewport.
     * Center the rectangle in the viewport.
     *
     * @param rectangle Rectangle relative to the base coordinate system
     */
    public void zoomToFitView(Rectangle rectangle) {
        JViewport viewport = (JViewport) this.getParent();
        Dimension portSize = viewport.getSize();

        // calculate the scale factor so that the rectangle fits in the viewport
        double factor = Math.min(
            Math.min(
                portSize.getWidth() / rectangle.getWidth(),
                portSize.getHeight() / rectangle.getHeight()),
            _maxScaling);

        // update the scaling factor
        changeScalingFactor(factor);

        // Transform with the new scaling factor
        Point rectangleCenter =
            new Point((int) rectangle.getCenterX(), (int) rectangle.getCenterY());
        _affinetransform.transform(rectangleCenter, rectangleCenter);

        // Translate the viewport so that the rectangle is centered in the viewport
        int xViewport = Math.max(rectangleCenter.x - portSize.width / 2, 0);
        int yViewport = Math.max(rectangleCenter.y - portSize.height / 2, 0);
        Point portPos = new Point(xViewport, yViewport);

        // For some reason the first call triggers some listener updates
        // that change the position back to something wrong. With the second
        // call that does not happen and the view is set to the desired position.
        viewport.setViewPosition(portPos);
        viewport.setViewPosition(portPos);

        // Force redraw of the whole graphic
        drawingInvalidated(new CH.ifa.draw.framework.DrawingChangeEvent(_drawing, getBounds()));
        checkDamage();
        viewport.repaint();
    }

    /**
     * Get the factor by which the graphics are scaled up or down
     * @return The current scaling factor
     */
    public double getScalingFactor() {
        return _scaleFactor;
    }

    /**
     * Change the factor by which the graphics are scaled up or down
     * @param newFactor The new scaling factor
     */
    public void changeScalingFactor(double newFactor) {
        _scaleFactor = newFactor;
        _affinetransform = AffineTransform.getScaleInstance(_scaleFactor, _scaleFactor);
        editor().showStatus("Zoom factor: " + Double.valueOf(newFactor * 100).intValue() + "%");
    }

    @Override
    public AffineTransform getAffineTransform() {
        return _affinetransform;
    }

    /**
     * Revert the transformation of a point.
     * <br>
     * Used for reversing the transformation of mouse event
     * coordinates in relation to an affine transformed Graphics object.
     */
    private Point inverseTransformPoint(Point p) {
        if (_affinetransform != null) {
            try {
                _affinetransform.inverseTransform(p, p);
            } catch (NoninvertibleTransformException e1) {
                LOGGER.error(e1.getMessage() + e1);
            }
        }

        return p;
    }

    /**
     * Handles key down events. Cursor keys are handled
     * by the view the other key events are delegated to the
     * currently active tool.
     */
    @Override
    public void keyPressed(KeyEvent e) {
        int code = e.getExtendedKeyCode();
        if ((code == KeyEvent.VK_BACK_SPACE) || (code == KeyEvent.VK_DELETE)) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(StandardDrawingView.class.getName() + ": KeyEvent e: Delete pressed");
            }
            // If there are selected figures
            if (!_selection.isEmpty()) {
                // If the alt key is pressed, set a flag to delete the figure's children as well
                if (e.isAltDown()) {
                    for (Figure figure : _selection) {
                        figure.setAttribute("DeleteWithChildren", Boolean.TRUE);
                    }
                } else {
                    // Otherwise remove the flag to prevent unwanted deletions
                    for (Figure figure : _selection) {
                        figure.setAttribute("DeleteWithChildren", null);
                    }
                }
                AbstractCommand cmd = new DeleteCommand("Delete");
                cmd.execute();
            }

        } else if (code == KeyEvent.VK_DOWN || code == KeyEvent.VK_UP || code == KeyEvent.VK_RIGHT
            || code == KeyEvent.VK_LEFT) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(StandardDrawingView.class.getName() + ": KeyEvent e: Arrow pressed");
            }
            handleCursorKey(code, e.getModifiersEx());


            // if we have a state with selected figures, pressing a cursor
            // key should only move the figures. If there are no selected
            // figures, the view of the viewport should scroll.
            if (!this._selection.isEmpty()) {
                // consume key event. This prevents scrolling of the ScrollPane.
                e.consume();
            }
        } else if ((e.getModifiersEx() & (Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()
            | InputEvent.SHIFT_DOWN_MASK)) == Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()
            && (code == KeyEvent.VK_0 || code == KeyEvent.VK_PLUS || code == KeyEvent.VK_MINUS)) {
            JViewport viewport = (JViewport) getParent();
            Point center = new Point(viewport.getWidth() / 2, viewport.getHeight() / 2);
            switch (code) {
                case KeyEvent.VK_0:
                    zoomOnPoint(1.0, center);
                    break;
                case KeyEvent.VK_PLUS:
                    zoomInOnPoint(center);
                    break;
                case KeyEvent.VK_MINUS:
                    zoomOutFromPoint(center);
                    break;
            }
        } else {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(
                    StandardDrawingView.class.getName()
                        + ": KeyEvent e: Pushing KeyEvent to tool e: " + code + " , "
                        + e.getKeyChar());
            }
            tool().keyDown(e, code);
        }
        checkDamage();
    }

    /**
     * Handles cursor keys by moving all the selected figures
     * one grid point in the cursor direction.
     */
    protected void handleCursorKey(int key) {
        this.handleCursorKey(key, 0);
    }

    /**
     * Handles cursor keys by moving all the selected figures
     * one grid point in the cursor direction.
     * <br>
     * If InputEvent.SHIFT_MASK is set in modifiers, take a bigger leap.
     */
    protected void handleCursorKey(int key, int modifiers) {
        int dx = 0;
        int dy = 0;
        int stepX = 1;
        int stepY = 1;

        // should consider Null Object.
        if (_constrainer != null) {
            stepX = _constrainer.getStepX();
            stepY = _constrainer.getStepY();
        }

        // if shift is pressed, move 10 times as far
        if ((modifiers & InputEvent.SHIFT_DOWN_MASK) != 0) {
            stepX *= 10;
            stepY *= 10;
        }

        switch (key) {
            case KeyEvent.VK_DOWN:
                dy = stepY;
                break;
            case KeyEvent.VK_UP:
                dy = -stepY;
                break;
            case KeyEvent.VK_RIGHT:
                dx = stepX;
                break;
            case KeyEvent.VK_LEFT:
                dx = -stepX;
                break;
        }
        _editor.prepareAccumulatedUndoSnapshot();
        moveSelection(dx, dy);
        _editor.triggerAccumulatedUndoSnapshot();
    }


    /**
     * Moves the given figures by the specified delta in the x and y directions.
     * A call to this method must always be followed by a call to checkDamage() for the affected views.
     *
     * @param figureVector the vector of figures to move
     * @param dx the horizontal movement delta
     * @param dy the vertical movement delta
     * @throws IllegalStateException throws IllegalStateException if null is encountered in the figureVector
     */
    public static void moveFigures(Vector<Figure> figureVector, int dx, int dy)
        throws IllegalStateException
    {
        Figure figure;
        Figure parent;
        Vector<Figure> iteratorFigureVector = new Vector<>(figureVector);

        for (Figure figure1 : figureVector) {
            if (figure1 instanceof CompositeAttributeFigure) {
                iteratorFigureVector.removeAll(((CompositeAttributeFigure) figure1).getAttached());
            }
        }

        FigureEnumeration figures = new FigureEnumerator(iteratorFigureVector);
        while (figures.hasMoreElements()) {
            figure = figures.nextFigure();
            parent = figure;
            if (figure == null) {
                throw new IllegalStateException();
            }
            while (parent != null) {
                if (parent instanceof ChildFigure) {
                    parent = ((ChildFigure) parent).parent();
                    if (parent != null && figureVector.contains(parent)) {
                        break;
                    }
                } else {
                    parent = null;
                }
            }
            if (parent == null) {
                figure.moveBy(dx, dy);
            }
        }
    }

    @Override
    public void moveSelection(int dx, int dy) {
        Point p = new Point(dx, dy);
        moveFigures(_selection, p.x, p.y);
        checkDamage();
    }

    /**
     * Checks whether the drawing has some accumulated damage
     * and informs all views about the required update,
     * if necessary.
     */
    @Override
    public synchronized void checkDamage() {
        Enumeration<DrawingChangeListener> each = drawing().drawingChangeListeners();
        while (each.hasMoreElements()) {
            Object l = each.nextElement();
            if (l instanceof DrawingView) {
                ((DrawingView) l).repairDamage();
            }
        }
    }

    @Override
    public void repairDamage() {
        synchronized (_repaintLock) {
            if (_damage != null) {
                Rectangle r = _damage;
                if (_affinetransform != null) {
                    r = _affinetransform.createTransformedShape(_damage).getBounds();
                }

                if ((r.x + r.width > r.width) || (r.y + r.height > r.height)) {
                    checkMinimumSize(r);
                }
                repaint(0L, r.x, r.y, r.width, r.height);
                _damage = null;
            }
        }
    }

    @Override
    public void drawingInvalidated(DrawingChangeEvent e) {
        synchronized (_repaintLock) {
            Rectangle r = e.getInvalidatedRectangle();

            if (r != null) {
                if (_damage == null) {
                    _damage = r;
                } else {
                    _damage.add(r);
                }
            }
        }
    }

    @Override
    public void drawingRequestUpdate(DrawingChangeEvent e) {
        repairDamage();
    }

    /**
     * Paints the drawing view. The actual drawing is delegated to
     * the current update strategy.
     * @see Painter
     */
    @Override
    public void paintComponent(Graphics g) {
        Graphics2D g2 = (Graphics2D) g;
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        // We are currently updating the view, so further
        // updates should wait a while.
        // chronized(repaintLock) {
        //    repaintActive = true;
        // }
        _updateStrategy.draw(g2, this);
        synchronized (_repaintLock) {
            // Any committed damage?
            if (_damage != null) {
                // Yes, handle the requested repaint in a separate
                // request. We cannot do the repaint directly, because
                // our graphics context might have an inappropriate
                // clip area.
                repaint(0L, _damage.x, _damage.y, _damage.width, _damage.height);
                _damage = null;
            }
        }
    }

    /**
     * Draws the contents of the drawing view.
     * The view has three layers: background, drawing, handles.
     * The layers are drawn in back to front order.
     */
    @Override
    public void drawAll(Graphics g) {
        boolean isPrinting = g instanceof PrintGraphics;
        drawBackground(g);
        if (_backgrounds != null && !isPrinting) {
            drawPainters(g, _backgrounds);
        }
        ContextGraphics cg = new ContextGraphics((Graphics2D) g, getBackground());

        cg.scale(_scaleFactor, _scaleFactor);
        //We need to change the mouse listeners as well if we want zoom
        drawDrawing(cg);
        if (_foregrounds != null && !isPrinting) {
            drawPainters(g, _foregrounds);
        }

        if (!isPrinting) {
            drawHandles(g);
        }

        cg.scale(1 / _scaleFactor, 1 / _scaleFactor);
        if (_drawing == _editor.drawing()) {
            tool().draw(cg);
        }
    }

    /**
     * Draws the currently active handles.
     * @param g graphic to be drawn into
     * @deprecated  This method is only for internal usage and will later be made private.
     */
    @Deprecated
    public void drawHandles(Graphics g) {
        Enumeration<FigureHandle> k = selectionHandles();
        while (k.hasMoreElements()) {
            k.nextElement().draw(g);
        }
    }

    /**
     * Draws the drawing.
     * @param g graphic to be drawn into
     */
    public void drawDrawing(Graphics g) {
        _drawing.draw(g);
    }

    /**
     * Draws the background. If a background pattern is set it
     * is used to fill the background. Otherwise, the background
     * is filled in the background color.
     * @param g graphic to be drawn into
     */
    public void drawBackground(Graphics g) {
        Color color = getBackgroundColor();
        g.setColor(color);
        Rectangle bounds = getBounds();
        g.fillRect(0, 0, bounds.width, bounds.height);
    }

    protected Color getBackgroundColor() {
        Color color = DockUI.getColor(LookAndFeelColors.PANEL_BACKGROUND);
        return color.brighter();
    }

    private void drawPainters(Graphics g, Vector<Painter> v) {
        for (int i = 0; i < v.size(); i++) {
            v.elementAt(i).draw(g, this);
        }
    }

    /**
     * Adds a background.
     */
    public void addBackground(Painter painter) {
        if (_backgrounds == null) {
            _backgrounds = new Vector<>(3);
        }
        _backgrounds.addElement(painter);
        repaint();
    }

    /**
     * Removes a background.
     */
    public void removeBackground(Painter painter) {
        if (_backgrounds != null) {
            _backgrounds.removeElement(painter);
        }
        repaint();
    }

    /**
     * Removes a foreground.
     */
    public void removeForeground(Painter painter) {
        if (_foregrounds != null) {
            _foregrounds.removeElement(painter);
        }
        repaint();
    }

    /**
     * Adds a foreground.
     */
    public void addForeground(Painter painter) {
        if (_foregrounds == null) {
            _foregrounds = new Vector<>(3);
        }
        _foregrounds.addElement(painter);
        repaint();
    }

    /**
     * Freezes the view by acquiring the drawing lock.
     * @see Drawing#lock
     */
    @Override
    public void freezeView() {
        //drawing().lock();
    }

    /**
     * Unfreezes the view by releasing the drawing lock.
     * @see Drawing#unlock
     */
    @Override
    public void unfreezeView() {
        //drawing().unlock();
    }

    @Serial
    private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
        s.defaultReadObject();

        _selection = new Vector<>(); // could use lazy initialization instead
        _selectionHandlesLock = new Object();
        _repaintLock = new Object();

        if (_drawing != null) {
            _drawing.addDrawingChangeListener(this);
        }
    }

    /**
     * Recalculates the size of the drawing and adapts
     * the view size, if the drawing size increased.
     * Does not adapt to a shrinking drawing size.
     *
     * @param area If <code>null</code>, all figures in
     *             the drawing will be inspected to determine
     *             the size of the drawing.
     *             <br>
     *             If a rectangle is given, its lower
     *             right corner will be used to determine
     *             the size. This is much faster, if the
     *             area is already known to the caller.
     **/
    private void checkMinimumSize(Rectangle area) {
        // Calculate (or estimate) the space occupied
        // by all figures in the drawing.
        if (area == null) {
            area = _drawing.getBounds();
        }
        Dimension d = new Dimension(area.x + area.width, area.y + area.height);


        // Adapt the size of the view to the size
        // of the drawing.
        if (_viewSize.height < d.height) {
            _viewSize.height = d.height + 10;
        }
        if (_viewSize.width < d.width) {
            _viewSize.width = d.width + 10;
        }


        // Also look at the bounds of the gui panel to
        // avoid grey areas outside the drawing.
        // But the gui bounds should not affect the
        // logical view size.
        Dimension guiSize = getSize();
        boolean changed = false;
        if (guiSize.width < _viewSize.width) {
            guiSize.width = _viewSize.width;
            changed = true;
        }
        if (guiSize.height < _viewSize.height) {
            guiSize.height = _viewSize.height;
            changed = true;
        }

        // Apply the changes, if necessary.
        if (changed) {
            setSize(guiSize.width, guiSize.height);
        }
    }

    @Override
    public boolean isFocusable() {
        return true;
    }

    // listener methods we are not interested in
    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}

    @Override
    public void mouseClicked(MouseEvent e) {}

    @Override
    public void keyTyped(KeyEvent e) {}

    @Override
    public void keyReleased(KeyEvent e) {}

    @Override
    public void showElement(Figure fig) {
        Component c = getParent();
        while ((c != null) && !(c instanceof JScrollPane)) {
            c = c.getParent();
        }

        if (c != null) {
            JScrollPane pane = (JScrollPane) c;
            Point p = fig.center();
            int x = p.x - (pane.getWidth() / 2);
            int y = p.y - (pane.getHeight() / 2);
            int vpWidth = pane.getViewport().getWidth();
            int vpHeight = pane.getViewport().getHeight();
            int vWidth = pane.getViewport().getView().getWidth();
            int vHeight = pane.getViewport().getView().getHeight();
            if (x < 0) {
                x = 0;
            }
            if (y < 0) {
                y = 0;
            }
            if (x + vpWidth > vWidth) {
                x = vWidth - vpWidth;
            }
            if (y + vpHeight > vHeight) {
                y = vHeight - vpHeight;
            }

            pane.getViewport().setViewPosition(new Point(x, y));
        }
    }

    /**
     * @see Printable#print(java.awt.Graphics, java.awt.print.PageFormat, int)
     */
    @Override
    public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) {
        if (pageIndex >= 1) {
            return Printable.NO_SUCH_PAGE;
        }
        if (graphics instanceof Graphics2D g2) {

            //set the upper left corner to imaginable corner on printer page
            g2.translate(pageFormat.getImageableX(), pageFormat.getImageableY());

            //Scaling
            double sH = pageFormat.getImageableHeight() / getHeight();
            double sW = pageFormat.getImageableWidth() / getWidth();
            double scale = Math.min(sH, sW);

            // do we need to scale?
            if (scale < 1) {
                g2.scale(scale, scale);
            }

            drawAll(graphics);
            return Printable.PAGE_EXISTS;
        }
        return Printable.NO_SUCH_PAGE;
    }
}
