package de.renew.gui.fs;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Enumeration;
import java.util.Vector;

import collections.CollectionEnumeration;
import collections.HashedMap;
import collections.HashedSet;
import collections.LinkedList;
import collections.UpdatableMap;
import collections.UpdatableSeq;
import collections.UpdatableSet;
import de.uni_hamburg.fs.BasicType;
import de.uni_hamburg.fs.EquivRelation;
import de.uni_hamburg.fs.FSNode;
import de.uni_hamburg.fs.FeatureStructure;
import de.uni_hamburg.fs.JavaObject;
import de.uni_hamburg.fs.ListType;
import de.uni_hamburg.fs.Name;
import de.uni_hamburg.fs.Node;
import de.uni_hamburg.fs.NullObject;
import de.uni_hamburg.fs.Path;
import de.uni_hamburg.fs.TagMap;
import de.uni_hamburg.fs.Type;

import CH.ifa.draw.standard.RelativeLocator;
import CH.ifa.draw.util.ColorMap;
import CH.ifa.draw.util.Fontkit;
import de.renew.draw.storables.ontology.StorableInput;
import de.renew.draw.storables.ontology.StorableOutput;
import de.renew.draw.ui.ontology.FigureHandle;
import de.renew.formalism.fs.FSNetCompiler;
import de.renew.formalism.fs.FSNetParser;
import de.renew.formalism.fs.FSNetPreprocessor;
import de.renew.formalism.fs.SingleFSNetCompiler;
import de.renew.gui.CPNTextFigure;
import de.renew.gui.NetInstanceHandle;
import de.renew.gui.SemanticUpdateFigure;
import de.renew.net.NetInstance;
import de.renew.remote.NetInstanceAccessor;
import de.renew.remote.RemotePlugin;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.SyntaxException;


/**
 * Class for figures which use FS-Semantics.
 */
public class FSFigure extends CPNTextFigure implements SemanticUpdateFigure {
    /**
     * Logger for the figure.
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(FSFigure.class);

    /**
     * Default size of a shutter.
     */
    public static final int SHUTTERSIZE = 7;

    /**
     * Default String to be inserted.
     */
    public static final String ELLIPSE = "...";

    transient private FSPlugin _fsPlugin = null;
    transient private boolean _alwaysUML = false;
    transient private boolean _automatic = false;
    transient private FeatureStructure _fs;
    transient private TagMap _tagmap;
    transient private FontMetrics _metrics;
    transient private FontMetrics _boldMetrics;
    private transient int _lineh;
    private transient int _d;
    private transient int _ascent;
    private transient Dimension _fExtent = null;
    private transient UpdatableSeq _subfigs = null; //of TextSubFigures
    private transient UpdatableSeq _boldfigs = null; //of TextSubFigures
    private transient UpdatableSet _openNodes = new HashedSet();

    // of Nodes which are openNodes
    private transient Node _selectedNode = null;
    private transient UpdatableSeq _handles = null; // of Tag- and ShutterHandles

    /**
     * If we only update handle rects, this variable contains the
     * current index within handles, -1 otherwise.
     **/
    private transient int _updateHandleIndex = -1;

    /**
     * Construct based on token.
     *
     * @param token token to be used for construction
     */
    public FSFigure(Object token) {
        this(new FeatureStructure(JavaObject.getJavaType(token)), false);
        _alwaysUML = true;
    }

    /**
     * Construct based on unexpanded feature.
     *
     * @param fs feature to be used for construction
     */
    public FSFigure(FeatureStructure fs) {
        this(fs, false);
    }

    /**
     * Construct based on expanded feature.
     *
     * @param fs feature to be used for construction
     * @param expanded whether the figure shall be expanded on construction
     */
    public FSFigure(FeatureStructure fs, boolean expanded) {
        super(INSCRIPTION);
        setFrameColor(Color.black); // so that TextFigure treats us as a box
        this._fs = fs;
        _automatic = true;
        _tagmap = new TagMap(fs.getRoot());
        setReadOnly(true);
        super.setText(" "); // non-empty, so that this figure is not removed!
        setAlignment(LEFT);
        //super.setText(fs.toString());
        if (expanded) {
            // initially all nodes are open:
            _openNodes.includeElements(fs.getNodes());


            // for JavaObjects, open first level:
            _openNodes.include(fs.getRoot());
        }
    }

    /**
     * Construct based on default values.
     */
    public FSFigure() {
        super(INSCRIPTION);
        setFrameColor(Color.black); // so that TextFigure treats us as a box
        setText("[]");
        setReadOnly(false);
    }

    @Override
    protected void basicSetText(String newText) {
        super.basicSetText(newText);
        if (!_automatic) {
            try {
                semanticUpdate(null);
            } catch (SyntaxException se) {
                _fs = null;
                LOGGER.error("Syntax Exception in Feature Structure:\n" + se);
            }
        }
    }

    FeatureStructure getFeatureStructure() {
        return _fs;
    }

    @Override
    public void semanticUpdate(ShadowNet shadowNet) throws SyntaxException {
        if (_automatic) {
            return;
        }
        synchronized (this) {
            _updateHandleIndex = -1;
            SingleFSNetCompiler compiler = new SingleFSNetCompiler();
            FSNetParser fsParser = new FSNetParser(new java.io.StringReader(getText()));
            if (shadowNet != null) {
                fsParser.setDeclarationNode(compiler.makeDeclarationNode(shadowNet));
            } else {
                fsParser.setDeclarationNode(null);
            }
            UpdatableMap tags = new HashedMap();
            EquivRelation er = new EquivRelation();
            _selectedNode = null;
            Node root = null;
            try {
                root = fsParser
                    .parseFS(tags, er, Path.EPSILON, new Vector<Path>(), new Vector<Object>());
            } catch (de.renew.formalism.java.ParseException ex) {
                SyntaxException e = FSNetCompiler.makeSyntaxException(ex);
                e.addObject(this);
                changed();
                throw e;
            }
            try {
                er.extensionalize();
                root = er.rebuild(root);
            } catch (Exception uff) {
                SyntaxException e = new SyntaxException("FS not extensionalizable!", uff);
                e.addObject(this);
                changed();
                throw e;
            }
            _fs = new FeatureStructure(root, false);
            _tagmap = new TagMap(root, er, tags);


            // logger.debug("Setting all FSNodes to open!");
            _openNodes = new HashedSet();


            // initially all fs-nodes are open:
            _openNodes.includeElements(_fs.getNodes());


            // for JavaObjects, open first level:
            _openNodes.include(root);
            changed();
            // logger.debug("FS: "+new FeatureStructure(fs));
        }
    }

    @Override
    public Vector<FigureHandle> handles() {
        Vector<FigureHandle> allHandles = super.handles();
        if (!_automatic && _fs != null && _fs.getFirstMissingAssociation() != null) {
            allHandles.addElement(new FeatureConnectionHandle(this, RelativeLocator.east()));
        }
        synchronized (this) {
            if (_handles != null) {
                CollectionEnumeration handleEnum = _handles.elements();
                while (handleEnum.hasMoreElements()) {
                    allHandles.addElement((FigureHandle) handleEnum.nextElement());
                }
            }
        }
        return allHandles;
    }

    /**
     * Get the bold version of a font.
     *
     * @param font font of which the bold version shall be retrieved
     * @return bold version of the font
     */
    public static Font getBoldFont(Font font) {
        return Fontkit.getFont(font.getName(), font.getStyle() | Font.BOLD, font.getSize());
    }

    /**
     * Initialises private variables in relationship with FontMetrics
     * to default values depending on the font of this FSFigure.
     */
    private void initMetrics() {
        _metrics = getDefaultFontMetrics(getFont());
        _boldMetrics = getDefaultFontMetrics(getBoldFont(getFont()));
        _lineh = _metrics.getHeight();
        _d = _metrics.stringWidth(" ");
        _ascent = _metrics.getAscent() - 1;
    }

    private boolean renderAsUml() {
        if (_alwaysUML) {
            return true;
        }
        if (_fsPlugin == null) {
            _fsPlugin = FSPlugin.getCurrent();
        }
        if (_fsPlugin != null) {
            return _fsPlugin.getUmlRenderMode();
        }
        return false;
    }

    /**
     * Setup of the figure.
     */
    public void setup() {
        boolean umlMode = renderAsUml();
        if (_fExtent == null) {
            synchronized (this) {
                _tagmap.resetVisited();
                initMetrics();
                _subfigs = new LinkedList();
                _boldfigs = new LinkedList();
                if (_updateHandleIndex < 0) {
                    _handles = new LinkedList();
                    // logger.debug("New handle list.");
                }
                Node root = _fs.getRoot();
                String tagstr = _tagmap.getTag(root).toString();

                // handle very special case: FS is one tag only:
                if (!umlMode && root.getType().equals(Type.TOP) && tagstr.length() > 0) {
                    if (!_handles.isEmpty()) {
                        _handles = new LinkedList();
                    }
                    int tagwidth = _metrics.stringWidth(tagstr) + _d;
                    _subfigs.insertFirst(new TextSubFigure(tagstr, (_d + 1) / 2, _ascent));
                    _subfigs.insertFirst(new FilledRectSubFigure(0, 0, tagwidth, _lineh - 2));
                    _fExtent = new Dimension(tagwidth, _lineh - 2);
                    _updateHandleIndex = -1;
                } else {
                    _fExtent = setupFS(0, 0, root, Type.TOP, umlMode);
                    _updateHandleIndex = 0;
                }

                //logger.debug("Setting update handle index to 0.");
            }
        }
    }

    @Override
    public Rectangle displayBox() {
        Rectangle box = super.displayBox();
        if (_fs == null) {
            return box;
        }
        setup();
        return new Rectangle(box.x, box.y, _fExtent.width, _fExtent.height);
    }

    @Override
    public void internalDraw(Graphics g) {
        if (_fs == null) {
            super.internalDraw(g);
        } else {
            synchronized (this) {
                setup();
                Rectangle box = super.displayBox();
                Color fill = getFillColor();
                if (!ColorMap.isTransparent(fill)) {
                    g.setColor(fill);
                    g.fillRect(box.x, box.y, _fExtent.width, _fExtent.height);
                }
                g.setColor((Color) getAttribute("TextColor"));
                g.setFont(getFont());
                g.translate(box.x, box.y);
                CollectionEnumeration subfigenumeration = _subfigs.elements();
                while (subfigenumeration.hasMoreElements()) {
                    ((Drawable) subfigenumeration.nextElement()).draw(g);
                }
                g.setFont(getBoldFont(getFont()));
                subfigenumeration = _boldfigs.elements();
                while (subfigenumeration.hasMoreElements()) {
                    ((Drawable) subfigenumeration.nextElement()).draw(g);
                }
                g.translate(-box.x, -box.y);
            }
        }
    }

    private boolean addShutter(Node fs, int x, int y) {
        boolean isClosed = !_openNodes.includes(fs);
        Rectangle shutterBox =
            new Rectangle(x - SHUTTERSIZE / 2, y - SHUTTERSIZE / 2, SHUTTERSIZE, SHUTTERSIZE);
        if (_updateHandleIndex < 0) {
            _handles.insertLast(new ShutterHandle(this, shutterBox, fs, isClosed));
        } else {
            ShutterHandle sh = (ShutterHandle) _handles.at(_updateHandleIndex++);
            if (sh._node != fs) {
                LOGGER.error(
                    "test failed: ShutterHandle references " + sh._node + " instead of " + fs);
            }
            sh.setBox(shutterBox);
            sh._isClosed = isClosed;
        }
        return isClosed;
    }

    private Dimension setupList(int x, int y, int maxh, Node fs, boolean umlMode) {
        int w = 0;
        Type listtype = fs.getType();
        try {
            ListType list = (ListType) listtype;
            Type elemtype = list.getBaseType();

            if (list.getSubtype() == ListType.NELIST) {
                if (_openNodes.includes(fs)) {
                    // list node is not closed:
                    Node head;
                    if (fs.hasFeature(ListType.HEAD)) {
                        head = fs.delta(ListType.HEAD);
                    } else {
                        head = new FSNode(elemtype);
                    }
                    Dimension headdim = setupFS(x, y, head, elemtype, umlMode);
                    maxh = Math.max(maxh, headdim.height);
                    w = headdim.width;

                    Node tail;
                    if (fs.hasFeature(ListType.TAIL)) {
                        tail = fs.delta(ListType.TAIL);
                    } else {
                        tail = new FSNode(ListType.getList(elemtype));
                    }
                    Dimension taildim = null;
                    Name tag = _tagmap.getTag(tail);
                    listtype = tail.getType();
                    if (listtype instanceof ListType) {
                        ListType tailtype = (ListType) listtype;
                        if (tailtype.getSubtype() == ListType.NELIST
                            && tag.equals(de.uni_hamburg.fs.Name.EMPTY)
                            && tailtype.getBaseType().equals(elemtype)
                            || tailtype.getSubtype() == ListType.ELIST) {
                            taildim = setupList(x + w + 2 * _d, y, maxh, tail, umlMode);
                            if (taildim.width > 0) {
                                w += 2 * _d;
                            }
                        }
                        if (taildim == null) {
                            taildim = setupFS(
                                x + w + 3 * _d - 1, y, tail, ListType.getList(elemtype), umlMode);


                            // add list seperator line:
                            _subfigs
                                .insertFirst(new LineSubFigure(x + w + 3 * _d / 2, y, 0, maxh - 1));
                            w += 3 * _d; // space for vertical line
                        }
                        w += taildim.width;
                        maxh = Math.max(maxh, taildim.height);
                    }
                }
            }
        } catch (ClassCastException cce) {
            LOGGER.error("!!!corrupted list! Unexpected type: " + listtype);
        }
        return new Dimension(w, maxh);
    }

    private Dimension setupFS(int x, int y, Node fs, Type type, boolean umlMode) {
        if (fs == null) {
            return new Dimension(0, 0);
        }
        int maxh = _lineh;
        int maxw = 0;
        Type nodetype = fs.getType();
        int left = x;
        int upper = y;
        boolean isNode = !(nodetype instanceof BasicType && ((BasicType) nodetype).isObject()
            || nodetype instanceof NullObject);
        boolean isList = false;
        if (nodetype instanceof ListType) {
            int subtype = ((ListType) nodetype).getSubtype();
            if ((subtype == ListType.ELIST || subtype == ListType.NELIST)
                && !_tagmap.isVisited(fs)) {
                isList = true;
            }
        }
        if (isNode) {
            left += _d; // space for square bracket
            if (isList) {
                left += _d; // additional space for angles
                maxh += 2; // increase default (empty) list height

                // convert types to base types:
                if (type instanceof ListType) {
                    type = ((ListType) type).getBaseType();
                }
                nodetype = ((ListType) nodetype).getBaseType();
            } else {
                upper += 2;
            }
        }
        boolean noFirstLine = true;
        Rectangle tagbox = null;
        Name tag = _tagmap.getTag(fs);
        if (!tag.equals(de.uni_hamburg.fs.Name.EMPTY)) {
            // tags are only drawn for taged nodes, not for basic types or
            // nodes without features of an extensional type.
            String tagstr = tag._name;
            if (nodetype instanceof JavaObject
                && ((JavaObject) nodetype).getJavaObject() instanceof NetInstance) {
                tagstr = ((JavaObject) nodetype).getJavaObject().toString();
            }
            int tagwidth;
            if (umlMode) {
                tagwidth = _boldMetrics.stringWidth(tagstr);
                if (nodetype.isInstanceType()) {
                    maxh += 1; // for underline
                    Drawable tagUL = new LineSubFigure(left, upper + _ascent + 1, tagwidth, 0);
                    _subfigs.insertFirst(tagUL);
                }
                _boldfigs.insertFirst(new TextSubFigure(tagstr, left, upper + _ascent));
            } else {
                tagwidth = _metrics.stringWidth(tagstr) + _d;
                _subfigs
                    .insertFirst(new TextSubFigure(tagstr, left + (_d + 1) / 2, upper + _ascent));
                Drawable tagBX = new FilledRectSubFigure(left, upper, tagwidth, _lineh - 2);
                _subfigs.insertFirst(tagBX);
            }
            tagbox = new Rectangle(left, upper, tagwidth, _lineh - 2);
            maxw = tagwidth;
            noFirstLine = false;
        }
        if (!isNode || !_tagmap.visit(fs)) {
            if (!isList && nodetype.getName().equals(FSNetPreprocessor.LINK)) {
                // special rendering of FS-Up-/Downlinks:
                int lx = 0;
                if (fs.hasFeature(FSNetPreprocessor.RCV)) {
                    Dimension rcvdim = setupFS(
                        x, y, fs.delta(FSNetPreprocessor.RCV),
                        nodetype.appropType(FSNetPreprocessor.RCV), umlMode);
                    maxh = rcvdim.height;
                    lx = rcvdim.width + _d;
                }
                Dimension subdim = setupFS(
                    x + lx + 2 * _d, y, fs.delta(FSNetPreprocessor.PARAM),
                    nodetype.appropType(FSNetPreprocessor.PARAM), umlMode);
                maxh = Math.max(maxh, subdim.height);
                maxw = lx + subdim.width + 2 * _d;
                _subfigs.insertFirst(new TextSubFigure(":", x + lx, y + _ascent));
                return new Dimension(maxw, maxh);
            } else {
                // draw optional first line with type (after tag)
                String typestr = "";
                if (!nodetype.equals(type)) {
                    typestr = nodetype.getName();
                }
                if (typestr.length() > 0) {
                    // logger.debug("nodetype: "+nodetype+", default type: "+type);
                    if (umlMode && isNode) {
                        typestr = ":" + typestr;
                    }
                    if (!umlMode && !tag.equals(de.uni_hamburg.fs.Name.EMPTY)) {
                        maxw += _d; // space between tag and type
                    }
                    Rectangle typeRect = new Rectangle(
                        left + maxw, upper, _boldMetrics.stringWidth(typestr), _lineh);
                    _boldfigs.insertFirst(new TextSubFigure(typestr, typeRect.x, upper + _ascent));
                    if (isNode && nodetype.isInstanceType()) {
                        _subfigs.insertFirst(
                            new LineSubFigure(left + maxw, upper + _ascent + 1, typeRect.width, 0));
                        if (!umlMode) {
                            maxh += 1; // space for underline
                        }
                    }
                    maxw += typeRect.width;
                    noFirstLine = false;
                }
                CollectionEnumeration features = fs.featureNames();
                boolean hasFeatures = features.hasMoreElements();
                if (hasFeatures) {
                    if (!_openNodes.includes(fs)) {
                        _subfigs
                            .insertFirst(new TextSubFigure(ELLIPSE, left + maxw, upper + _ascent));
                        maxw += _metrics.stringWidth(ELLIPSE);
                    } else {
                        if (noFirstLine) {
                            maxh = 0;
                        } else if (umlMode && !noFirstLine) {
                            maxh += 1; // space for separating line
                        }
                        if (isList) {
                            int listdx = 1;
                            int listdy = maxh;
                            if (typestr.length() == 0 && maxw > 0) {
                                // place list right of Tag:
                                listdx = maxw + _d;
                                listdy = 0;
                            }
                            Dimension listdim =
                                setupList(left + listdx, upper + listdy, 0, fs, umlMode);
                            maxw = Math.max(maxw, listdx + listdim.width);
                            maxh = Math.max(maxh, listdy + listdim.height);
                        } else {
                            do {
                                Name featureName = (Name) features.nextElement();
                                String feature = featureName.toString() + (umlMode ? "=" : ": ");
                                int indent = _metrics.stringWidth(feature);
                                Dimension subdim = setupFS(
                                    left + indent, upper + maxh, fs.delta(featureName),
                                    nodetype.appropType(featureName), umlMode);
                                _subfigs.insertFirst(
                                    new TextSubFigure(
                                        feature, left,
                                        upper + maxh + (subdim.height - _lineh) / 2 + _ascent));
                                maxw = Math.max(maxw, indent + subdim.width);
                                maxh += subdim.height + 1; // + d;
                            } while (features.hasMoreElements());
                            maxh += 1;
                            if (umlMode && !noFirstLine) {
                                _subfigs.insertFirst(
                                    new LineSubFigure(x, y + _lineh + 1, maxw + 2 * _d - 1, 0));
                            }
                        }
                    }
                }
                if (hasFeatures) {
                    if (isList) {
                        addShutter(fs, x, y + maxh / 2);
                    } else {
                        addShutter(fs, x, y + _lineh / 2);
                    }
                }
            }
        }
        if (maxw == 0) {
            maxw = _d;
        }
        if (isNode) {
            maxw += 2 * _d;
            if (isList) {
                maxw += 2 * _d;
            } else {
                maxh += 2;
            }
            Drawable tagsubfigure;
            if (umlMode && !isList) {
                _subfigs.insertFirst(new FilledRectSubFigure(x, y, maxw, maxh));
                tagsubfigure = new RectSubFigure(x, y, maxw, maxh);
            } else {
                tagsubfigure =
                    new BracketSubFigure(x, y, maxw, maxh, isList ? 2 * _d - 1 : _d - 1, isList);
                _subfigs.insertFirst(tagsubfigure);
            }
            if (tagbox != null) {
                boolean successfulNetInstanceHandle = false;
                if (nodetype instanceof JavaObject
                    && ((JavaObject) nodetype).getJavaObject() instanceof NetInstance) {
                    {
                        if (_updateHandleIndex < 0) {
                            // TODO Check whether the remote layer should
                            // be used in a more general way than just
                            // wrapping a local object.
                            NetInstanceAccessor niacc = RemotePlugin.getInstance().wrapInstance(
                                (NetInstance) ((JavaObject) nodetype).getJavaObject());
                            _handles.insertLast(new NetInstanceHandle(this, tagbox, niacc));
                        } else {
                            NetInstanceHandle nih =
                                (NetInstanceHandle) _handles.at(_updateHandleIndex++);
                            nih.setBox(tagbox);
                        }
                        successfulNetInstanceHandle = true;
                    }
                }

                if (!successfulNetInstanceHandle) {
                    if (_updateHandleIndex < 0) {
                        // create new tag handles:
                        _handles.insertLast(
                            new TagHandle(
                                this, tagbox, tagsubfigure, fs, fs.equals(_selectedNode)));
                    } else {
                        // update existing tag handles:
                        // logger.debug("Updating handle "+updateHandleIndex);
                        TagHandle th = (TagHandle) _handles.at(_updateHandleIndex++);
                        if (th._node != fs) {
                            LOGGER.error(
                                "test failed: TagHandle references " + th._node + " instead of "
                                    + fs);
                        }
                        th.setBox(tagbox);
                        th.setHighlight(tagsubfigure);
                        th._selected = fs.equals(_selectedNode);
                    }
                }
            }
        }
        return new Dimension(maxw, maxh);
    }

    @Override
    public void read(StorableInput dr) throws IOException {
        if (dr.getVersion() <= 5) {
            // Hack for old FS-Figures:
            super.readWithoutType(dr);
        } else {
            super.read(dr);
            setFrameColor(Color.black); // so that TextFigure treats us as a box
            if (dr.getVersion() > 6) {
                // read list of paths for open nodes:
                _openNodes = new HashedSet();
                int noPaths = dr.readInt();
                Node root = null;
                if (_fs != null) {
                    root = _fs.getRoot();
                }
                while (noPaths-- > 0) {
                    String pathStr = dr.readString();
                    if (root != null) {
                        Path path = new Path(pathStr);
                        _openNodes.include(root.delta(path));
                    }
                }
            }
        }
    }

    @Override
    public void write(StorableOutput dw) {
        super.write(dw);
        if (_fs == null || _openNodes == null) {
            dw.writeInt(0);
        } else {
            dw.writeInt(_openNodes.size());
            CollectionEnumeration nodes = _openNodes.elements();
            while (nodes.hasMoreElements()) {
                dw.writeString(_fs.onePathTo((Node) nodes.nextElement()).toString());
            }
        }
    }

    @Override
    protected void markDirty() {
        _fExtent = null;
        super.markDirty();
    }

    private Enumeration<Node> getListNodes(Node node) {
        Vector<Node> listNodes = new Vector<Node>();
        listNodes.addElement(node);
        Type listType = node.getType();
        if (listType instanceof ListType && ((ListType) listType).getSubtype() == ListType.NELIST) {
            while (true) {
                node = node.delta(ListType.TAIL);
                if (!node.getType().equals(listType) || listNodes.contains(node)) {
                    break;
                }
                listNodes.addElement(node);
            }
        }
        return listNodes.elements();
    }

    void setNodeShutState(Node node, boolean close, boolean deep) {
        synchronized (this) {
            Enumeration<Node> nodes;
            if (deep) {
                nodes = new FeatureStructure(node, false).getNodes();
            } else {
                nodes = getListNodes(node);
            }
            if (shutOrClose(nodes, close)) {
                handlesChanged();
                invalidate();
                markDirty();
                _selectedNode = null;
                _updateHandleIndex = -1;
                changed();
            }
        }
    }

    private boolean shutOrClose(Enumeration<Node> nodes, boolean close) {
        boolean changed = false;
        while (nodes.hasMoreElements()) {
            changed |= shutOrClose(nodes.nextElement(), close);
        }
        return changed;
    }

    private boolean shutOrClose(Node node, boolean shut) {
        boolean isClosed = !_openNodes.includes(node);
        if (isClosed == shut) {
            return false;
        }
        if (shut) {
            _openNodes.removeOneOf(node);
        } else {
            _openNodes.include(node);
        }
        return true;
    }

    void setSelectedTag(Node node) {
        synchronized (this) {
            if (node.equals(_selectedNode)) {
                // already selected, toggle to unselected
                _selectedNode = null;
            } else {
                _selectedNode = node;
            }
            invalidate();
            CollectionEnumeration tags = _handles.elements();
            while (tags.hasMoreElements()) {
                Object handle = tags.nextElement();
                if (handle instanceof TagHandle) {
                    TagHandle th = (TagHandle) handle;
                    th._selected = th._node.equals(_selectedNode);
                }
            }
            if (_selectedNode != null) {
                // make all tagrects of this tag visible:
                if (shutOrClose(_fs.backwardsReachableNodes(_selectedNode), false)) {
                    handlesChanged();
                    markDirty();
                    _updateHandleIndex = -1;
                }
            }
            changed();
        }
    }

    /**
     * Reads an object into text.
     *
     * @param in Stream of objects to be read
     * @throws IOException thrown when objects cannot be opened
     * @throws ClassNotFoundException thrown when object cannot be cast to an existing class
     */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        in.defaultReadObject();
        basicSetText(getText());
    }
}
