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

package CH.ifa.draw.figures;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.util.StringTokenizer;
import java.util.Vector;

import org.apache.log4j.Logger;

import CH.ifa.draw.framework.ChildFigure;
import CH.ifa.draw.framework.ParentFigure;
import CH.ifa.draw.standard.AbstractFigure;
import CH.ifa.draw.standard.FigureEnumerator;
import CH.ifa.draw.standard.MergedFigureEnumerator;
import CH.ifa.draw.standard.NullHandle;
import CH.ifa.draw.standard.OffsetLocator;
import CH.ifa.draw.standard.RelativeLocator;
import CH.ifa.draw.standard.TextHolder;
import CH.ifa.draw.util.ColorMap;
import CH.ifa.draw.util.ExtendedFont;
import CH.ifa.draw.util.Fontkit;
import CH.ifa.draw.util.GUIProperties;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureChangeEvent;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.draw.storables.ontology.StorableInput;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.draw.ui.ontology.FigureHandle;

/**
 * A text figure.
 * <p>
 * {@link TextTool}
 */
public class TextFigure extends AttributeFigure implements ChildFigure, TextHolder {
    /** Logger used for the class */
    public static final Logger logger = Logger.getLogger(TextFigure.class);

    /**
     * Current Font name of this class.
     */
    private static String _currentFontName = "SansSerif";

    /**
     * Current Font size of this class.
     */
    private static int _currentFontSize = 12;

    /**
     * Current Font style of this class.
     */
    private static int _currentFontStyle = Font.PLAIN;
    /*
     * Serialization support.
     */
    @Serial
    private static final long serialVersionUID = 4599820785949456124L;

    /**
     * The name of the attribute which determines the text
     * alignment. To query or set the attribute, you can use
     * the <code>set</code>- or <code>getAlignment()</code>
     * methods. But it is also possible to use <code>set</code>-
     * or <code>getAttribute()</code> with this name.
     * <p>
     * Valid values for the attribute are {@link #LEFT},
     * {@link #CENTER} and {@link #RIGHT}.
     * </p>
     **/
    public static final String ALIGN_ATTR = "TextAlignment";

    /**
     * Specifies left-justified text, if set as {@link #ALIGN_ATTR}.
     */
    public static final int LEFT = 0;

    /**
     * Specifies centered text, if set as {@link #ALIGN_ATTR}.
     */
    public static final int CENTER = 1;

    /**
     * Specifies right-justified text, if set as {@link #ALIGN_ATTR}.
     */
    public static final int RIGHT = 2;

    static {
        int defaultFontSize = GUIProperties.defaultFontSize();
        if (defaultFontSize != -1) {
            _currentFontSize = defaultFontSize;
            logger.debug("Setting default font size to " + _currentFontSize + " pt.");
        }
        String defaultFontName = GUIProperties.defaultFontName();
        if (!defaultFontName.equals("none")) {
            _currentFontName = defaultFontName;
            logger.debug("Setting default font to " + _currentFontName + ".");
        }
    }

    /**
     * The x coordinate of the upper left corner of this
     * figure.
     *
     * @serial
     **/
    private int _originX;

    /**
     * The y coordinate of the upper left corner of this
     * figure.
     *
     * @serial
     **/
    private int _originY;

    // cache of the TextFigure's size
    private transient boolean _sizeIsDirty = true;
    private transient int _width;
    private transient int _height;
    private transient Rectangle[] _boxes; // cache for display boxes of lines

    /**
     * The text to be displayed as a whole.
     *
     * @serial
     **/
    private String _text;

    /**
     * The text to be displayed, broken into lines.
     * <p>
     * This field is derived from <code>fText</code> each
     * time the text is modified by <code>setText()</code>.
     * It won't be written to a <code>StorableOutput</code>,
     * instead it will be recalculated in the read method.
     * However, the field gets serialized.
     * </p>
     *
     * @serial
     **/
    private String[] _lines;

    /**
     * The font used to draw the text.
     * If <code>null</code>, the font will be retrieved
     * by using the values of <code>fCurrentFontName</code>,
     * -<code>Size</code> and -<code>Style</code>.
     * Therefore, this field is transient.
     **/
    private transient Font _font = null;

    /**
     * The name of the font to be used to display the text.
     * This field can be queried or modified as an attribute
     * named <code>"FontName"</code>.
     *
     * @serial
     **/
    private String _attributeFontName = _currentFontName;

    /**
     * The size of the font to be used to display the text.
     * This field can be queried or modified as an attribute
     * named <code>"FontSize"</code>.
     *
     * @serial
     **/
    private int _attributeFontSize;

    /**
     * The style of the font to be used to display the text.
     * This field can be queried or modified as an attribute
     * named <code>"FontStyle"</code>.
     *
     * @serial
     **/
    private int _attributeFontStyle = _currentFontStyle;

    /**
     * Determines whether the text can be edited.
     *
     * @serial
     **/
    private boolean _isReadOnly;

    /**
     * Determines whether this text figure can be connected
     * to other figures as a {@link ChildFigure}.
     * <p>
     * When written to a <code>StorableOutput</code>, this
     * field is omitted. Instead, it will default on restoration
     * to <code>true</code>, if it is currently connected,
     * to a figure, and <code>false</code>, if it's not.
     * However, the field is serialized.
     * </p>
     *
     * @serial
     **/
    private boolean _canBeConnected = true;

    /**
     * The figure to which this text figure is attached.
     * if <code>null</code>, it is not attached.
     * <p>
     * On restoration from <code>StorableInput</code> (but
     * not on deserialization), the parent figure has to
     * be informed about this child again.
     * </p>
     *
     * @serial
     * @see AbstractFigure#children
     * @see AbstractFigure#addChild
     **/
    private ParentFigure _parent = null;

    /**
     * This locator is used only if the text figure is
     * connected to a parent figure. Then it determines
     * the location of this text figure by pointing to
     * its center (or where the center should be).
     *
     * @serial
     */
    private OffsetLocator _locator = null;

    /**
     * Unused serial version number.
     */
    @SuppressWarnings("unused")
    private final int _textFigureSerializedDataVersion = 1;

    /**
     * Constructs a new TextFigure with empty text.
     */
    public TextFigure() {
        this("");
    }

    /**
     * Constructs a new TextFigure with empty text.
     *
     * @param canBeConnected boolean whether it can be Connected to other figures
     */
    public TextFigure(boolean canBeConnected) {
        this("");
        _canBeConnected = canBeConnected;
    }

    /**
     * Constructs a new TextFigure with initial text content.
     *
     * @param text String for the initial text content
     */
    public TextFigure(String text) {
        _attributeFontSize = _currentFontSize;
        _originX = 0;
        _originY = 0;
        setAttribute("FillColor", ColorMap.color("None"));
        setAttribute("FrameColor", ColorMap.color("None"));
        internalSetText(text);
        _sizeIsDirty = true;
    }

    /**
     * Method to get the default metrics for any given Font.
     *
     * @param font font which default metrics should be calculated
     * @return default font metrics for given font
     */
    protected static FontMetrics getDefaultFontMetrics(Font font) {
        BufferedImage bi = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB_PRE);
        Graphics g = bi.getGraphics();
        FontMetrics fm = g.getFontMetrics(font);
        g.dispose();
        return fm;
    }

    /**
     * Constructor for initializing the object with initial text and custom readability.
     *
     * @param text text that will be set as initial content
     * @param isReadOnly sets the readability of the object
     */
    public TextFigure(String text, boolean isReadOnly) {
        this(text);
        setReadOnly(isReadOnly);
    }

    @Override
    public void moveBy(int x, int y) {
        willChange();
        basicMoveBy(x, y);
        if (_locator != null) {
            _locator.moveBy(x, y);
        }
        changed();
    }

    @Override
    protected void basicMoveBy(int x, int y) {
        _originX += x;
        _originY += y;
    }

    @Override
    public void displayBox(Point origin, Point corner) {
        willChange();
        int xMovedBy = origin.x - _originX;
        int yMovedBy = origin.y - _originY;
        basicDisplayBox(origin, corner);
        if (_locator != null) {
            _locator.moveBy(xMovedBy, yMovedBy);
        }
        changed();
    }

    @Override
    public void basicDisplayBox(Point newOrigin, Point newCorner) {
        _originX = newOrigin.x;
        _originY = newOrigin.y;
    }

    /**
     * Getter method for getting a Point representing the origin.
     *
     * @return Point object for the origin
     */
    public Point getOrigin() {
        return new Point(_originX, _originY);
    }

    @Override
    public Rectangle displayBox() {
        textExtent();
        return new Rectangle(_originX, _originY, _width, _height);
    }

    @Override
    public Rectangle textDisplayBox() {
        return displayBox();
    }

    /**
     * Getter method for getting one specific line a certain index.
     *
     * @param lineIndex The index at which the line will be returned
     * @return The string that represent the corresponding line at the index
     */
    protected String getLine(int lineIndex) {
        return _lines[lineIndex];
    }

    //NOTICE signature

    /**
     * Returns the text alignment for the line of text specified by the given index.
     * <p>
     * By default, this method is identical to {@link #getAlignment()}.
     * Subclasses may override this method to get different behavior for different indices.
     *
     * @param lineIndex index of the line whose alignment is requested
     * @return Integer value of the Attribute determining the text alignment
     */
    protected int getLineAlignment(int lineIndex) {
        return getAlignment();
    }

    //NOTICE signature

    /**
     * Returns the font for the line of text specified by the given index.
     * <p>
     * By default, this method is identical to {@link #getFont()}.
     * Subclasses may override this method to get different behavior for different indices.
     *
     * @param lineIndex index of the line whose alignment is requested
     * @return Integer value of the Attribute determining the text alignment
     */
    protected Font getLineFont(int lineIndex) {
        return getFont();
    }

    /**
     * Method to obtain FontMetrics for a specified Font.
     *
     * @param font Font that should be used
     * @param g used Graphics object
     * The current Graphics context. null if no Graphics object is available.
     * @return Font metrics specified by the Font and Graphics.
     * If Graphics is null default FontMetrics will be returned.
     */
    public static FontMetrics getMetrics(Font font, Graphics g) {
        if (g != null) {
            return g.getFontMetrics(font);
        }
        logger.trace("Using default font metrics because no Graphics object is at hand. ");
        return getDefaultFontMetrics(font);
    }

    /**
     * Method to get The Rectangle box surrounding a text line.
     * If the size of the font has been changed, the line boxes are recalculated first.
     *
     * @param g Graphics object to draw on
     * @param i the line index for which the box will be returned
     * @return Rectangle surrounding the line at index i
     */
    public Rectangle getLineBox(Graphics g, int i) {
        if (_sizeIsDirty) {
            getLineBoxes(g);
        }
        return _boxes[i];
    }

    /**
     * Method to get a Dimension object, representing the size of The line at index i
     * with the respective Font of the line.
     *
     * @param i the index of the current line.
     * @param g graphics context, could be null.
     * @return Dimension of the line (width, height)
     */
    protected Dimension getLineDimension(int i, Graphics g) {
        FontMetrics metrics = getMetrics(getLineFont(i), g); //NOTICE signature
        return new Dimension(metrics.stringWidth(getLine(i)), metrics.getHeight());
    }

    /**
     * Used to calculate rectangles for every text line, that can surround the line entirely.
     *
     * @param g graphics context, could be null.
     * @return For every line of this TextFigure the Rectangle that encloses that line.
     */
    protected Rectangle[] getLineBoxes(Graphics g) {
        if (_sizeIsDirty) {
            _boxes = new Rectangle[_lines.length];
            int oldWidth = _width;
            _width = 0;
            _height = 0;
            for (int i = 0; i < _lines.length; i++) {
                Dimension dim = getLineDimension(i, g);
                _boxes[i] = new Rectangle(0, _height, dim.width, dim.height);
                _width = Math.max(_width, dim.width);
                _height += dim.height;
            }
            for (int i = 0; i < _lines.length; i++) {
                int alignment = getLineAlignment(i); //NOTICE signature
                if (alignment != LEFT) {
                    int dx = _width - _boxes[i].width;
                    if (alignment == CENTER) {
                        dx /= 2;
                    }
                    _boxes[i].translate(dx, 0);
                }
            }

            // As a side effect, reposition the whole text figure
            // according to its alignment. This should have an
            // effect only if the width of the figure has changed.
            if (oldWidth != 0) {
                switch (getAlignment()) {
                    case RIGHT:
                        _originX = _originX + oldWidth - _width;
                        if (_locator != null) {
                            _locator.moveBy((oldWidth - _width) / 2, 0);
                        }
                        break;
                    case CENTER:
                        _originX = _originX + (oldWidth - _width) / 2;
                        // offsetLocator remains unchanged, when centered
                        break;
                    default:
                        // fOriginX remains unchanged, when left-justified.
                        if (_locator != null) {
                            _locator.moveBy(-(oldWidth - _width) / 2, 0);
                        }
                        break;
                }
            }
            _sizeIsDirty = false;
        }
        return _boxes;
    }

    /**
     * Checks if a point is inside the figure.
     */
    @Override
    public boolean containsPoint(int x, int y) {
        if (super.containsPoint(x, y)) {
            if (!ColorMap.isTransparent(getFrameColor())) {
                return true; // regard as box!
            }

            // check if the point lies within some line's box:
            x -= _originX;
            y -= _originY;
            Rectangle[] boxes = getLineBoxes(null);
            for (Rectangle box : boxes) {
                if (box.contains(x, y)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns if this Figure is read-only or not.
     *
     * @return boolean indicating whether the figure is read only
     */
    public boolean readOnly() {
        return _isReadOnly;
    }

    /**
     * Sets the read only status of the text figure.
     */
    @Override
    public void setReadOnly(boolean isReadOnly) {
        _isReadOnly = isReadOnly;
    }

    /**
     * Gets the font.
     */
    @Override
    public Font getFont() {
        if (_font == null) {
            _font = Fontkit.getFont(_attributeFontName, _attributeFontStyle, _attributeFontSize);
        }
        return _font;
    }

    /**
     * Sets the font.
     *
     * @param newFont Font that should be used
     */
    public void setFont(Font newFont) {
        willChange();
        _font = newFont;
        _attributeFontName = _font.getName();
        _attributeFontStyle = _font.getStyle();
        _attributeFontSize = _font.getSize();
        markDirty();
        changed();
    }

    /**
     * Updates the location whenever the figure changes itself.
     */
    @Override
    public void changed() {
        super.changed();
        updateLocation();
    }

    /**
     * Gets the value of the "TextAlignment" attribute.
     *
     * @return Integer value of the Attribute determining the text alignment
     */
    public int getAlignment() {
        return (Integer) getAttribute(ALIGN_ATTR);
    }

    /**
     * Sets the value of the "TextAlignment" attribute.
     *
     * @param alignment the int value that the Attribute will be set to
     */
    public void setAlignment(int alignment) {
        setAttribute(ALIGN_ATTR, alignment);
    }

    /**
     * A text figure understands the "FontSize", "FontStyle", and "FontName"
     * attributes.
     */
    @Override
    public Object getAttribute(String name) {
        return switch (name) {
            case "FontSize" -> _attributeFontSize;
            case "FontStyle" -> _attributeFontStyle;
            case "FontName" -> _attributeFontName;
            default -> super.getAttribute(name);
        };
    }

    /**
     * A text figure understands the "FontSize", "FontStyle", and "FontName"
     * attributes (which are applied directly to the figure instead of
     * setting them as attribute values).
     * The {@link #ALIGN_ATTR} attribute is set like any ordinary attribute,
     * but the appearance must be adapted afterward.
     */
    @Override
    public void setAttribute(String name, Object value) {
        if (name.equals("FontSize") || name.equals("FontStyle") || name.equals("FontName")) {
            willChange();
            if (name.equals("FontSize")) {
                _attributeFontSize = (Integer) value;
            } else if (name.equals("FontStyle")) {
                int s = (Integer) value;
                if (s == Font.PLAIN) {
                    _attributeFontStyle = Font.PLAIN;
                } else {
                    _attributeFontStyle = _attributeFontStyle ^ s;
                }
            } else {
                _attributeFontName = (String) value;
            }
            _font = null;
            markDirty();
            changed();
            markDirty();
            changed();
        } else {
            super.setAttribute(name, value);
            if (name.equals(ALIGN_ATTR)) {
                willChange();
                markDirty();
                changed();
            }
        }
    }

    /**
     * Gets the text shown by the text figure.
     */
    @Override
    public String getText() {
        return _text;
    }

    /**
     * Get the text shown by the text figure, but split into
     * an array of individual lines.
     *
     * @return an array of strings, one for each line of text
     */
    public String[] getLines() {
        return _lines;
    }

    /**
     * Sets the text shown by the text figure.
     */
    @Override
    public void setText(String newText) {
        if (!newText.equals(_text)) {
            willChange();
            basicSetText(newText);
            changed();
        }
    }

    /**
     * Sets new Text and marks the object as dirty.
     *
     * @param newText String that represent the new displayed Text
     */
    protected void basicSetText(String newText) {
        internalSetText(newText);
        markDirty();
    }

    /**
     * Sets new Text and breaks it into fitting lines.
     *
     * @param newText String that represent the new displayed Text
     */
    protected void internalSetText(String newText) {
        _text = newText;
        _lines = splitTextLines(newText);
    }

    /**
     * Sets new internal Text while setting another Text to be broken into lines and be displayed.
     *
     * @param fullText String that will be the internal text representation
     * @param displayText String that will be broken into lines and displayed
     */
    protected void internalSetTextHiddenParts(String fullText, String displayText) {
        _text = fullText;
        _lines = splitTextLines(displayText);
    }

    /**
     * Splits a given String into different lines to be displayed, keeping empty lines.
     *
     * <p>The delimiter used is "\n" and the method ensures each line is returned as a separate string.
     * if the input ends with a delimiter, an empty line is added to the end of the Array
     * </p>
     *
     * @param text String that should be split
     * @return Array of Strings, each representing one line (might be empty)
     */
    private static String[] splitTextLines(String text) {
        Vector<String> vector = new Vector<>();
        StringTokenizer lines = new StringTokenizer(text, "\n", true);
        boolean lastLineTerminated = true;
        while (lines.hasMoreElements()) {
            String line = lines.nextToken();
            if ("\n".equals(line)) {
                line = "";
                lastLineTerminated = true;
            } else if (lines.hasMoreElements()) {
                // it was not only a separator, skip its separator:
                lines.nextToken();
                lastLineTerminated = true;
            } else {
                lastLineTerminated = false;
            }
            vector.addElement(line);
        }
        if (lastLineTerminated) {
            vector.addElement("");
        }
        String[] arr = new String[vector.size()];
        vector.copyInto(arr);
        return arr;
    }

    /**
     * Tests whether the figure accepts typing.
     */
    @Override
    public boolean acceptsTyping() {
        return !_isReadOnly;
    }

    @Override
    public void internalDraw(Graphics g) {
        super.internalDraw(g);
        Color fillColor = getFillColor();
        getLineBoxes(g); // recalculate if dirty
        g.translate(_originX, _originY);
        if (ColorMap.isTransparent(getFrameColor()) && !ColorMap.isTransparent(fillColor)) {
            // fill each line individually:
            g.setColor(fillColor);
            for (int i = 0; i < _lines.length; i++) {
                g.fillRect(_boxes[i].x, _boxes[i].y, _boxes[i].width, _boxes[i].height);
            }
        }
        Color textColor = (Color) getAttribute("TextColor");
        if (!ColorMap.isTransparent(textColor)) {
            g.setColor(textColor);
            for (int i = 0; i < _lines.length; i++) {
                drawLine(g, i);
            }
        }
        g.translate(-_originX, -_originY);
    }

    /**
     * Draws a line on the screen.
     *
     * @param g The Graphics context.
     * @param i The index of the line to be drawn.
     */
    protected void drawLine(Graphics g, int i) {
        Font font = getLineFont(i); //NOTICE signature
        g.setFont(font);

        int x = _boxes[i].x;
        int y = _boxes[i].y;
        String s = getLine(i);
        // There is a bug in jdk1.1.7 drawString:
        // String is not properly converted to different encodings.  
        //
        // However, there is another bug in the Sun JDK:
        // drawBytes does not respect the font before drawString
        // is called once.
        //g.drawString(" ",-10000,-10000);
        // Use drawBytes instead of drawString?
        g.drawString(s, x, y + getMetrics(font, g).getAscent());
        if (font instanceof ExtendedFont && ((ExtendedFont) font).isUnderlined()) {
            FontMetrics fm = g.getFontMetrics();
            LineMetrics lm = fm.getLineMetrics(s, g);
            Graphics2D g2 = (Graphics2D) g;
            Shape shape = new Rectangle2D.Float(
                x, y + (int) lm.getUnderlineOffset() + getMetrics(font, g).getAscent(),
                fm.stringWidth(s), Math.max(1, (int) lm.getUnderlineThickness()));
            g2.fill(shape);
        }
    }

    @Override
    public void drawBackground(Graphics g) {
        if (!ColorMap.isTransparent(getFrameColor())) {
            Rectangle r = displayBox();
            Graphics2D g2 = (Graphics2D) g;
            Shape s = new Rectangle2D.Float(r.x, r.y, r.width, r.height);
            g2.fill(s);
        }
    }

    @Override
    public void drawFrame(Graphics g) {
        Rectangle r = displayBox();
        Graphics2D g2 = (Graphics2D) g;
        Shape s = new Rectangle2D.Float(r.x, r.y, r.width, r.height);
        g2.draw(s);
    }

    /**
     * Returns the width and height of the TextFigure in the form of a {@link Dimension}.
     *
     * @return Dimension representing the width and height of the TextFigure
     */
    private Dimension textExtent() {
        getLineBoxes(null);
        return new Dimension(_width, _height);
    }

    /**
     * Marks the size as dirty, meaning it was altered.
     */
    protected void markDirty() {
        _sizeIsDirty = true;
    }

    /**
     * Gets the number of rows and columns to be overlaid when the figure
     * is edited.
     */
    @Override
    public Dimension overlayRowsAndColumns() {
        int columns = 20;
        String[] lines = getLines();
        for (String line : lines) {
            columns = Math.max(columns, line.length() + 3);
        }
        int rows = Math.max(1, lines.length);

        return new Dimension(columns, rows);
    }

    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> handles = new Vector<>();
        handles.addElement(new NullHandle(this, RelativeLocator.northWest()));
        handles.addElement(new NullHandle(this, RelativeLocator.northEast()));
        handles.addElement(new NullHandle(this, RelativeLocator.southEast()));
        handles.addElement(new FontSizeHandle(this, RelativeLocator.southWest()));
        return handles;
    }

    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        dw.writeInt(_originX);
        dw.writeInt(_originY);
        dw.writeString(_text);
        dw.writeString(_attributeFontName);
        dw.writeInt(_attributeFontStyle);
        dw.writeInt(_attributeFontSize);
        dw.writeBoolean(_isReadOnly);
        dw.writeStorable(_parent);
        dw.writeStorable(_locator);
    }

    @Override
    public void read(StorableInput dr) throws IOException {
        super.read(dr);
        _originX = dr.readInt();
        _originY = dr.readInt();
        String text = dr.readString();
        setText(text);
        _attributeFontName = dr.readString();
        _attributeFontStyle = dr.readInt();
        _attributeFontSize = dr.readInt();

        // The default font changed, because "Helvetica"
        // is no longer supported by Java 1.3. Patching
        // of older files is necessary.
        if (dr.getVersion() < 8) {
            if ("Helvetica".equals(_attributeFontName)) {
                _attributeFontName = "SansSerif";
            }
        }
        _font = null;
        _isReadOnly = dr.readBoolean();

        _parent = (ParentFigure) dr.readStorable();
        _locator = (OffsetLocator) dr.readStorable();
        if (_parent != null) {
            _parent.addChild(this);
        } else {
            // patch: doesn't have a parent=>can't have one!
            _canBeConnected = false;
        }
        if (GUIProperties.noGraphics()) {
            _sizeIsDirty = false;
        } else {
            updateLocation();
        }
    }

    /**
     * Set-Up method for object deserialization.
     * Restores parent child-relationship during the process.
     *
     * @param s the input stream from which the object is deserialized
     * @throws ClassNotFoundException if the class can not be found
     * @throws IOException when an I/O error occurs during handling
     */
    @Serial
    private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
        s.defaultReadObject();

        if (_parent != null) {
            _parent.addChild(this);
        }
        markDirty();
    }

    @Override
    public boolean canBeParent(ParentFigure figure) {
        if (!_canBeConnected) {
            return figure == null;
        }
        while (figure != null) {
            if (figure == this) {
                return false; // cyclic!!!
            }
            if (figure instanceof ChildFigure) {
                figure = ((ChildFigure) figure).parent();
            } else {
                break;
            }
        }
        return true;
    }

    @Override
    public boolean setParent(ParentFigure figure) {
        if (!canBeParent(figure)) {
            return false;
        }
        if (_parent != null) {
            _parent.removeChild(this);
        }
        _parent = figure;
        if (_parent == null) {
            _locator = null;
        } else {
            _parent.addChild(this);
            if (_locator != null) {
                _locator.setBase(_parent.connectedTextLocator(this));
            } else {
                _locator = new OffsetLocator(_parent.connectedTextLocator(this));
            }
            updateLocation();
        }
        return true;

    }

    @Override
    public ParentFigure parent() {
        return _parent;
    }

    /**
     * Updates the location relative to the connected figure.
     * The TextFigure is centered around the located point.
     */
    @Override
    public void updateLocation() {
        if (_locator != null) {
            Point p = _locator.locate(_parent);
            p.x -= size().width / 2 + _originX;
            p.y -= size().height / 2 + _originY;

            if (p.x != 0 || p.y != 0) {
                willChange();
                basicMoveBy(p.x, p.y);
                changed();
            }
        }
    }

    @Override
    public void release() {
        super.release();
        if (_parent != null) {
            _parent.removeChild(this);
        }
    }

    /**
     * Creates the current font to be used for new text figures.
     *
     * @return Font instance configured with the current font name, style and size
     */
    static public Font createCurrentFont() {
        return Fontkit.getFont(_currentFontName, _currentFontStyle, _currentFontSize);
    }

    /**
     * Sets the current font name.
     *
     * @param name that the font should be set to
     */
    static public void setCurrentFontName(String name) {
        _currentFontName = name;
    }

    /**
     * Sets the current font size.
     *
     * @param size that should be used for the Font
     */
    static public void setCurrentFontSize(int size) {
        _currentFontSize = size;
    }

    /**
     * Sets the current font style.
     *
     * @param style that should be used for the Font
     */
    static public void setCurrentFontStyle(int style) {
        _currentFontStyle = style;
    }

    @Override
    public void figureChanged(FigureChangeEvent e) {
        updateLocation();
    }

    @Override
    public void figureHandlesChanged(FigureChangeEvent e) {}

    @Override
    public void figureRemoved(FigureChangeEvent e) {
        if (listener() != null) {
            listener().figureRequestRemove(StorableApi.createFigureChangeEvent(this));
        }
    }

    @Override
    public void figureRequestRemove(FigureChangeEvent e) {}

    @Override
    public void figureInvalidated(FigureChangeEvent e) {}

    @Override
    public void figureRequestUpdate(FigureChangeEvent e) {}

    @Override
    public FigureEnumeration getFiguresWithDependencies() {
        FigureEnumeration superDep = super.getFiguresWithDependencies();
        Vector<Figure> myDep = new Vector<>(1);
        myDep.addElement(parent());
        return new MergedFigureEnumerator(superDep, new FigureEnumerator(myDep));
    }

    @Override
    public boolean inspect(DrawingView view, boolean alternate) {
        if (!alternate) {
            return super.inspect(view, false);
        } else {
            if (acceptsTyping()) {
                view.editor().doTextEdit(this);
                return true;
            }
            return false;
        }
    }
}