/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.bridge.svg12;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;
import java.util.TreeSet;
import java.util.List;
import java.util.Map;

import javax.swing.event.EventListenerList;

import org.apache.batik.anim.dom.BindableElement;
import org.apache.batik.anim.dom.XBLEventSupport;
import org.apache.batik.anim.dom.XBLOMContentElement;
import org.apache.batik.anim.dom.XBLOMDefinitionElement;
import org.apache.batik.anim.dom.XBLOMImportElement;
import org.apache.batik.anim.dom.XBLOMShadowTreeElement;
import org.apache.batik.anim.dom.XBLOMTemplateElement;
import org.apache.batik.bridge.BridgeContext;
import org.apache.batik.bridge.BridgeException;
import org.apache.batik.bridge.ErrorConstants;
import org.apache.batik.dom.AbstractAttrNS;
import org.apache.batik.dom.AbstractDocument;
import org.apache.batik.dom.AbstractNode;
import org.apache.batik.dom.events.NodeEventTarget;
import org.apache.batik.dom.xbl.NodeXBL;
import org.apache.batik.dom.xbl.ShadowTreeEvent;
import org.apache.batik.dom.xbl.XBLManager;
import org.apache.batik.dom.xbl.XBLManagerData;
import org.apache.batik.dom.xbl.XBLShadowTreeElement;
import org.apache.batik.util.DoublyIndexedTable;
import org.apache.batik.util.XBLConstants;
import org.apache.batik.util.XMLConstants;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.events.DocumentEvent;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.events.MutationEvent;

/**
 * A full featured sXBL manager.
 *
 * @author <a href="mailto:cam%40mcc%2eid%2eau">Cameron McCormack</a>
 * @version $Id: DefaultXBLManager.java 1733416 2016-03-03 07:07:13Z gadams $
 */
public class DefaultXBLManager implements XBLManager, XBLConstants {

    /**
     * Whether XBL processing is currently taking place.
     */
    protected boolean isProcessing;

    /**
     * The document.
     */
    protected Document document;

    /**
     * The BridgeContext.
     */
    protected BridgeContext ctx;

    /**
     * Map of namespace URI/local name pairs to ordered sets of
     * definition records.
     */
    protected DoublyIndexedTable definitionLists = new DoublyIndexedTable();

    /**
     * Map of definition element/import element pairs to definition records.
     */
    protected DoublyIndexedTable definitions = new DoublyIndexedTable();

    /**
     * Map of shadow trees to content managers.
     */
    protected Map contentManagers = new HashMap();

    /**
     * Map of import elements to import records.
     */
    protected Map imports = new HashMap();

    /**
     * DOM node inserted listener for the document.
     */
    protected DocInsertedListener docInsertedListener
        = new DocInsertedListener();

    /**
     * DOM node removed listener for the document.
     */
    protected DocRemovedListener docRemovedListener
        = new DocRemovedListener();

    /**
     * DOM subtree mutation listener for the document.
     */
    protected DocSubtreeListener docSubtreeListener
        = new DocSubtreeListener();

    /**
     * DOM attribute listener for import elements.
     */
    protected ImportAttrListener importAttrListener = new ImportAttrListener();

    /**
     * DOM attribute listener for referencing definition elements.
     */
    protected RefAttrListener refAttrListener = new RefAttrListener();

    /**
     * Global event listener list for XBL binding related events.
     */
    protected EventListenerList bindingListenerList = new EventListenerList();

    /**
     * Global event listener list for ContentSelectionChanged events.
     */
    protected EventListenerList contentSelectionChangedListenerList
        = new EventListenerList();

    /**
     * Creates a new DefaultXBLManager for the given document.
     */
    public DefaultXBLManager(Document doc, BridgeContext ctx) {
        document = doc;
        this.ctx = ctx;
        ImportRecord ir = new ImportRecord(null, null);
        imports.put(null, ir);
    }

    /**
     * Starts XBL processing on the document.
     */
    public void startProcessing() {
        if (isProcessing) {
            return;
        }

        // Get list of all current definitions in the document.
        NodeList nl = document.getElementsByTagNameNS(XBL_NAMESPACE_URI,
                                                      XBL_DEFINITION_TAG);
        XBLOMDefinitionElement[] defs
            = new XBLOMDefinitionElement[nl.getLength()];
        for (int i = 0; i < defs.length; i++) {
            defs[i] = (XBLOMDefinitionElement) nl.item(i);
        }

        // Get list of all imports in the document.
        nl = document.getElementsByTagNameNS(XBL_NAMESPACE_URI,
                                             XBL_IMPORT_TAG);
        Element[] imports
            = new Element[nl.getLength()];
        for (int i = 0; i < imports.length; i++) {
            imports[i] = (Element) nl.item(i);
        }

        // Add document listeners.
        AbstractDocument doc = (AbstractDocument) document;
        XBLEventSupport es = (XBLEventSupport) doc.initializeEventSupport();
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeRemoved",
             docRemovedListener, true);
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeInserted",
             docInsertedListener, true);
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMSubtreeModified",
             docSubtreeListener, true);

        // Add definitions.
        for (int i = 0; i < defs.length; i++) {
            if (defs[i].getAttributeNS(null, XBL_REF_ATTRIBUTE).length() != 0) {
                addDefinitionRef(defs[i]);
            } else {
                String ns = defs[i].getElementNamespaceURI();
                String ln = defs[i].getElementLocalName();
                addDefinition(ns, ln, defs[i], null);
            }
        }

        // Add imports.
        for (int i = 0; i < imports.length; i++) {
            addImport(imports[i]);
        }

        // Bind all of the bindable elements in the document that have a
        // matching definition.
        isProcessing = true;
        bind(document.getDocumentElement());
    }

    /**
     * Stops XBL processing on the document.
     */
    public void stopProcessing() {
        if (!isProcessing) {
            return;
        }
        isProcessing = false;

        // Remove document listeners.
        AbstractDocument doc = (AbstractDocument) document;
        XBLEventSupport es = (XBLEventSupport) doc.initializeEventSupport();
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeRemoved",
             docRemovedListener, true);
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeInserted",
             docInsertedListener, true);
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMSubtreeModified",
             docSubtreeListener, true);

        // Remove all imports.
        int nSlots = imports.values().size();
        ImportRecord[] irs = new ImportRecord[ nSlots ];
        imports.values().toArray( irs );
        for (int i = 0; i < irs.length; i++) {
            ImportRecord ir = irs[i];
            if (ir.importElement.getLocalName().equals(XBL_DEFINITION_TAG)) {
                removeDefinitionRef(ir.importElement);
            } else {
                removeImport(ir.importElement);
            }
        }

        // Remove all bindings.
        Object[] defRecs = definitions.getValuesArray();
        definitions.clear();
        for (int i = 0; i < defRecs.length; i++) {
            DefinitionRecord defRec = (DefinitionRecord) defRecs[i];
            TreeSet defs = (TreeSet) definitionLists.get(defRec.namespaceURI,
                                                         defRec.localName);
            if (defs != null) {
                while (!defs.isEmpty()) {
                    defRec = (DefinitionRecord) defs.first();
                    defs.remove(defRec);
                    removeDefinition(defRec);
                }
                definitionLists.put(defRec.namespaceURI, defRec.localName, null);
            }
        }
        definitionLists = new DoublyIndexedTable();
        contentManagers.clear();
    }

    /**
     * Returns whether XBL processing is currently enabled.
     */
    public boolean isProcessing() {
        return isProcessing;
    }

    /**
     * Adds a definition through its referring definition element (one
     * with a 'ref' attribute).
     */
    protected void addDefinitionRef(Element defRef) {
        String ref = defRef.getAttributeNS(null, XBL_REF_ATTRIBUTE);
        Element e = ctx.getReferencedElement(defRef, ref);
        if (!XBL_NAMESPACE_URI.equals(e.getNamespaceURI())
                || !XBL_DEFINITION_TAG.equals(e.getLocalName())) {
            throw new BridgeException
                (ctx, defRef, ErrorConstants.ERR_URI_BAD_TARGET,
                 new Object[] { ref });
        }
        ImportRecord ir = new ImportRecord(defRef, e);
        imports.put(defRef, ir);

        NodeEventTarget et = (NodeEventTarget) defRef;
        et.addEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMAttrModified",
             refAttrListener, false, null);

        XBLOMDefinitionElement d = (XBLOMDefinitionElement) defRef;
        String ns = d.getElementNamespaceURI();
        String ln = d.getElementLocalName();
        addDefinition(ns, ln, (XBLOMDefinitionElement) e, defRef);
    }

    /**
     * Removes a definition through its referring definition element (one
     * with a 'ref' attribute).
     */
    protected void removeDefinitionRef(Element defRef) {
        ImportRecord ir = (ImportRecord) imports.get(defRef);
        NodeEventTarget et = (NodeEventTarget) defRef;
        et.removeEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMAttrModified",
             refAttrListener, false);
        DefinitionRecord defRec
            = (DefinitionRecord) definitions.get(ir.node, defRef);
        removeDefinition(defRec);
        imports.remove(defRef);
    }

    /**
     * Imports bindings from another document.
     */
    protected void addImport(Element imp) {
        String bindings = imp.getAttributeNS(null, XBL_BINDINGS_ATTRIBUTE);
        Node n = ctx.getReferencedNode(imp, bindings);
        if (n.getNodeType() == Node.ELEMENT_NODE
                && !(XBL_NAMESPACE_URI.equals(n.getNamespaceURI())
                        && XBL_XBL_TAG.equals(n.getLocalName()))) {
            throw new BridgeException
                (ctx, imp, ErrorConstants.ERR_URI_BAD_TARGET,
                 new Object[] { n });
        }
        ImportRecord ir = new ImportRecord(imp, n);
        imports.put(imp, ir);

        NodeEventTarget et = (NodeEventTarget) imp;
        et.addEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMAttrModified",
             importAttrListener, false, null);

        et = (NodeEventTarget) n;
        et.addEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMNodeInserted",
             ir.importInsertedListener, false, null);
        et.addEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMNodeRemoved",
             ir.importRemovedListener, false, null);
        et.addEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMSubtreeModified",
             ir.importSubtreeListener, false, null);
        addImportedDefinitions(imp, n);
    }

    /**
     * Adds the definitions in the given imported subtree.
     */
    protected void addImportedDefinitions(Element imp, Node n) {
        if (n instanceof XBLOMDefinitionElement) {
            XBLOMDefinitionElement def = (XBLOMDefinitionElement) n;
            String ns = def.getElementNamespaceURI();
            String ln = def.getElementLocalName();
            addDefinition(ns, ln, def, imp);
        } else {
            n = n.getFirstChild();
            while (n != null) {
                addImportedDefinitions(imp, n);
                n = n.getNextSibling();
            }
        }
    }

    /**
     * Removes an import.
     */
    protected void removeImport(Element imp) {
        ImportRecord ir = (ImportRecord) imports.get(imp);
        NodeEventTarget et = (NodeEventTarget) ir.node;
        et.removeEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMNodeInserted",
             ir.importInsertedListener, false);
        et.removeEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMNodeRemoved",
             ir.importRemovedListener, false);
        et.removeEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMSubtreeModified",
             ir.importSubtreeListener, false);

        et = (NodeEventTarget) imp;
        et.removeEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI, "DOMAttrModified",
             importAttrListener, false);

        Object[] defRecs = definitions.getValuesArray();
        for (int i = 0; i < defRecs.length; i++) {
            DefinitionRecord defRec = (DefinitionRecord) defRecs[i];
            if (defRec.importElement == imp) {
                removeDefinition(defRec);
            }
        }
        imports.remove(imp);
    }

    /**
     * Adds an xbl:definition element to the list of definitions that
     * could possibly affect elements with the specified QName.  This
     * may or may not actually cause a new binding to come in to effect,
     * as this new definition element may be added earlier in the
     * document than another already in effect.
     *
     * @param namespaceURI the namespace URI of the bound elements
     * @param localName the local name of the bound elements
     * @param def the xbl:definition element
     * @param imp the xbl:import or xbl;definition element through which
     *            this definition is being added, or null if the binding
     *            is in the original document
     */
    protected void addDefinition(String namespaceURI,
                                 String localName,
                                 XBLOMDefinitionElement def,
                                 Element imp) {
        ImportRecord ir = (ImportRecord) imports.get(imp);
        DefinitionRecord oldDefRec = null;
        DefinitionRecord defRec;
        TreeSet defs = (TreeSet) definitionLists.get(namespaceURI, localName);
        if (defs == null) {
            defs = new TreeSet();
            definitionLists.put(namespaceURI, localName, defs);
        } else if (defs.size() > 0) {
            oldDefRec = (DefinitionRecord) defs.first();
        }
        XBLOMTemplateElement template = null;
        for (Node n = def.getFirstChild(); n != null; n = n.getNextSibling()) {
            if (n instanceof XBLOMTemplateElement) {
                template = (XBLOMTemplateElement) n;
                break;
            }
        }
        defRec = new DefinitionRecord(namespaceURI, localName, def,
                                      template, imp);
        defs.add(defRec);
        definitions.put(def, imp, defRec);
        addDefinitionElementListeners(def, ir);
        if (defs.first() != defRec) {
            return;
        }
        if (oldDefRec != null) {
            XBLOMDefinitionElement oldDef = oldDefRec.definition;
            XBLOMTemplateElement oldTemplate = oldDefRec.template;
            if (oldTemplate != null) {
                removeTemplateElementListeners(oldTemplate, ir);
            }
            removeDefinitionElementListeners(oldDef, ir);
        }
        if (template != null) {
            addTemplateElementListeners(template, ir);
        }
        if (isProcessing) {
            rebind(namespaceURI, localName, document.getDocumentElement());
        }
    }

    /**
     * Adds DOM mutation listeners to the given definition element.
     */
    protected void addDefinitionElementListeners(XBLOMDefinitionElement def,
                                                 ImportRecord ir) {
        XBLEventSupport es = (XBLEventSupport) def.initializeEventSupport();
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMAttrModified",
             ir.defAttrListener, false);
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeInserted",
             ir.defNodeInsertedListener, false);
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeRemoved",
             ir.defNodeRemovedListener, false);
    }

    /**
     * Adds DOM mutation listeners to the given template element.
     */
    protected void addTemplateElementListeners(XBLOMTemplateElement template,
                                               ImportRecord ir) {
        XBLEventSupport es
            = (XBLEventSupport) template.initializeEventSupport();
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMAttrModified",
             ir.templateMutationListener, false);
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeInserted",
             ir.templateMutationListener, false);
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeRemoved",
             ir.templateMutationListener, false);
        es.addImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMCharacterDataModified",
             ir.templateMutationListener, false);
    }

    /**
     * Removes an xbl:definition element from the list of definitions that
     * could possibly affect elements with the specified QName.  This
     * will only cause a new binding to come in to effect if it is currently
     * active.
     */
    protected void removeDefinition(DefinitionRecord defRec) {
        TreeSet defs = (TreeSet) definitionLists.get(defRec.namespaceURI,
                                                     defRec.localName);
        if (defs == null) {
            return;
        }
        Element imp = defRec.importElement;
        ImportRecord ir = (ImportRecord) imports.get(imp);
        DefinitionRecord activeDefRec = (DefinitionRecord) defs.first();
        defs.remove(defRec);
        definitions.remove(defRec.definition, imp);
        removeDefinitionElementListeners(defRec.definition, ir);
        if (defRec != activeDefRec) {
            return;
        }
        if (defRec.template != null) {
            removeTemplateElementListeners(defRec.template, ir);
        }
        rebind(defRec.namespaceURI, defRec.localName,
               document.getDocumentElement());
    }

    /**
     * Removes DOM mutation listeners from the given definition element.
     */
    protected void removeDefinitionElementListeners
            (XBLOMDefinitionElement def,
             ImportRecord ir) {
        XBLEventSupport es = (XBLEventSupport) def.initializeEventSupport();
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMAttrModified",
             ir.defAttrListener, false);
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeInserted",
             ir.defNodeInsertedListener, false);
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeRemoved",
             ir.defNodeRemovedListener, false);
    }

    /**
     * Removes DOM mutation listeners from the given template element.
     */
    protected void removeTemplateElementListeners
            (XBLOMTemplateElement template,
             ImportRecord ir) {
        XBLEventSupport es
            = (XBLEventSupport) template.initializeEventSupport();
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMAttrModified",
             ir.templateMutationListener, false);
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeInserted",
             ir.templateMutationListener, false);
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMNodeRemoved",
             ir.templateMutationListener, false);
        es.removeImplementationEventListenerNS
            (XMLConstants.XML_EVENTS_NAMESPACE_URI,
             "DOMCharacterDataModified",
             ir.templateMutationListener, false);
    }

    /**
     * Returns the definition record of the active definition for namespace
     * URI/local name pair.
     */
    protected DefinitionRecord getActiveDefinition(String namespaceURI,
                                                   String localName) {
        TreeSet defs = (TreeSet) definitionLists.get(namespaceURI, localName);
        if (defs == null || defs.size() == 0) {
            return null;
        }
        return (DefinitionRecord) defs.first();
    }

    /**
     * Unbinds each bindable element in the given element's subtree.
     */
    protected void unbind(Element e) {
        if (e instanceof BindableElement) {
            setActiveDefinition((BindableElement) e, null);
        } else {
            NodeList nl = getXblScopedChildNodes(e);
            for (int i = 0; i < nl.getLength(); i++) {
                Node n = nl.item(i);
                if (n.getNodeType() == Node.ELEMENT_NODE) {
                    unbind((Element) n);
                }
            }
        }
    }

    /**
     * Binds each bindable element in the given element's subtree.
     */
    protected void bind(Element e) {
        AbstractDocument doc = (AbstractDocument) e.getOwnerDocument();
        if (doc != document) {
            XBLManager xm = doc.getXBLManager();
            if (xm instanceof DefaultXBLManager) {
                ((DefaultXBLManager) xm).bind(e);
                return;
            }
        }

        if (e instanceof BindableElement) {
            DefinitionRecord defRec
                = getActiveDefinition(e.getNamespaceURI(),
                                      e.getLocalName());
            setActiveDefinition((BindableElement) e, defRec);
        } else {
            NodeList nl = getXblScopedChildNodes(e);
            for (int i = 0; i < nl.getLength(); i++) {
                Node n = nl.item(i);
                if (n.getNodeType() == Node.ELEMENT_NODE) {
                    bind((Element) n);
                }
            }
        }
    }

    /**
     * Rebinds each bindable element of the given name in the given element's
     * subtree.
     */
    protected void rebind(String namespaceURI, String localName, Element e) {
        AbstractDocument doc = (AbstractDocument) e.getOwnerDocument();
        if (doc != document) {
            XBLManager xm = doc.getXBLManager();
            if (xm instanceof DefaultXBLManager) {
                ((DefaultXBLManager) xm).rebind(namespaceURI, localName, e);
                return;
            }
        }

        if (e instanceof BindableElement
                && namespaceURI.equals(e.getNamespaceURI())
                && localName.equals(e.getLocalName())) {
            DefinitionRecord defRec
                = getActiveDefinition(e.getNamespaceURI(),
                                      e.getLocalName());
            setActiveDefinition((BindableElement) e, defRec);
        } else {
            NodeList nl = getXblScopedChildNodes(e);
            for (int i = 0; i < nl.getLength(); i++) {
                Node n = nl.item(i);
                if (n.getNodeType() == Node.ELEMENT_NODE) {
                    rebind(namespaceURI, localName, (Element) n);
                }
            }
        }
    }

    /**
     * Sets the given definition as the active one for a particular
     * bindable element.
     */
    protected void setActiveDefinition(BindableElement elt,
                                       DefinitionRecord defRec) {
        XBLRecord rec = getRecord(elt);
        rec.definitionElement = defRec == null ? null : defRec.definition;
        if (defRec != null
                && defRec.definition != null
                && defRec.template != null) {
            setXblShadowTree(elt, cloneTemplate(defRec.template));
        } else {
            setXblShadowTree(elt, null);
        }
    }

    /**
     * Sets the shadow tree for the given bindable element.
     */
    protected void setXblShadowTree(BindableElement elt,
                                    XBLOMShadowTreeElement newShadow) {
        XBLOMShadowTreeElement oldShadow
            = (XBLOMShadowTreeElement) getXblShadowTree(elt);
        if (oldShadow != null) {
            fireShadowTreeEvent(elt, XBL_UNBINDING_EVENT_TYPE, oldShadow);
            ContentManager cm = getContentManager(oldShadow);
            if (cm != null) {
                cm.dispose();
            }
            elt.setShadowTree(null);
            XBLRecord rec = getRecord(oldShadow);
            rec.boundElement = null;
            oldShadow.removeEventListenerNS
                (XMLConstants.XML_EVENTS_NAMESPACE_URI,
                 "DOMSubtreeModified",
                 docSubtreeListener, false);
        }
        if (newShadow != null) {
            newShadow.addEventListenerNS
                (XMLConstants.XML_EVENTS_NAMESPACE_URI,
                 "DOMSubtreeModified",
                 docSubtreeListener, false, null);
            fireShadowTreeEvent(elt, XBL_PREBIND_EVENT_TYPE, newShadow);
            elt.setShadowTree(newShadow);
            XBLRecord rec = getRecord(newShadow);
            rec.boundElement = elt;
            AbstractDocument doc
                = (AbstractDocument) elt.getOwnerDocument();
            XBLManager xm = doc.getXBLManager();
            ContentManager cm = new ContentManager(newShadow, xm);
            setContentManager(newShadow, cm);
        }
        invalidateChildNodes(elt);
        if (newShadow != null) {
            NodeList nl = getXblScopedChildNodes(elt);
            for (int i = 0; i < nl.getLength(); i++) {
                Node n = nl.item(i);
                if (n.getNodeType() == Node.ELEMENT_NODE) {
                    bind((Element) n);
                }
            }
            dispatchBindingChangedEvent(elt, newShadow);
            fireShadowTreeEvent(elt, XBL_BOUND_EVENT_TYPE, newShadow);
        } else {
            dispatchBindingChangedEvent(elt, newShadow);
        }
    }

    /**
     * Fires a ShadowTreeEvent of the given type on this element.
     */
    protected void fireShadowTreeEvent(BindableElement elt,
                                       String type,
                                       XBLShadowTreeElement e) {
        DocumentEvent de = (DocumentEvent) elt.getOwnerDocument();
        ShadowTreeEvent evt
            = (ShadowTreeEvent) de.createEvent("ShadowTreeEvent");
        evt.initShadowTreeEventNS(XBL_NAMESPACE_URI, type, true, false, e);
        elt.dispatchEvent(evt);
    }

    /**
     * Clones a template element for use as a shadow tree.
     */
    protected XBLOMShadowTreeElement cloneTemplate
            (XBLOMTemplateElement template) {
        XBLOMShadowTreeElement clone =
            (XBLOMShadowTreeElement)
            template.getOwnerDocument().createElementNS(XBL_NAMESPACE_URI,
                                                        XBL_SHADOW_TREE_TAG);
        NamedNodeMap attrs = template.getAttributes();
        for (int i = 0; i < attrs.getLength(); i++) {
            Attr attr = (Attr) attrs.item(i);
            if (attr instanceof AbstractAttrNS) {
                clone.setAttributeNodeNS(attr);
            } else {
                clone.setAttributeNode(attr);
            }
        }
        for (Node n = template.getFirstChild();
                n != null;
                n = n.getNextSibling()) {
            clone.appendChild(n.cloneNode(true));
        }
        return clone;
    }

    /**
     * Get the parent of a node in the fully flattened tree.
     */
    public Node getXblParentNode(Node n) {
        Node contentElement = getXblContentElement(n);
        Node parent = contentElement == null
                        ? n.getParentNode()
                        : contentElement.getParentNode();
        if (parent instanceof XBLOMContentElement) {
            parent = parent.getParentNode();
        }
        if (parent instanceof XBLOMShadowTreeElement) {
            parent = getXblBoundElement(parent);
        }
        return parent;
    }

    /**
     * Get the list of child nodes of a node in the fully flattened tree.
     */
    public NodeList getXblChildNodes(Node n) {
        XBLRecord rec = getRecord(n);
        if (rec.childNodes == null) {
            rec.childNodes = new XblChildNodes(rec);
        }
        return rec.childNodes;
    }

    /**
     * Get the list of child nodes of a node in the fully flattened tree
     * that are within the same shadow scope.
     */
    public NodeList getXblScopedChildNodes(Node n) {
        XBLRecord rec = getRecord(n);
        if (rec.scopedChildNodes == null) {
            rec.scopedChildNodes = new XblScopedChildNodes(rec);
        }
        return rec.scopedChildNodes;
    }

    /**
     * Get the first child node of a node in the fully flattened tree.
     */
    public Node getXblFirstChild(Node n) {
        NodeList nl = getXblChildNodes(n);
        return nl.item(0);
    }

    /**
     * Get the last child node of a node in the fully flattened tree.
     */
    public Node getXblLastChild(Node n) {
        NodeList nl = getXblChildNodes(n);
        return nl.item(nl.getLength() - 1);
    }

    /**
     * Get the node which directly precedes a node in the xblParentNode's
     * xblChildNodes list.
     */
    public Node getXblPreviousSibling(Node n) {
        Node p = getXblParentNode(n);
        if (p == null || getRecord(p).childNodes == null) {
            return n.getPreviousSibling();
        }
        XBLRecord rec = getRecord(n);
        if (!rec.linksValid) {
            updateLinks(n);
        }
        return rec.previousSibling;
    }

    /**
     * Get the node which directly follows a node in the xblParentNode's
     * xblChildNodes list.
     */
    public Node getXblNextSibling(Node n) {
        Node p = getXblParentNode(n);
        if (p == null || getRecord(p).childNodes == null) {
            return n.getNextSibling();
        }
        XBLRecord rec = getRecord(n);
        if (!rec.linksValid) {
            updateLinks(n);
        }
        return rec.nextSibling;
    }

    /**
     * Get the first element child of a node in the fully flattened tree.
     */
    public Element getXblFirstElementChild(Node n) {
        n = getXblFirstChild(n);
        while (n != null && n.getNodeType() != Node.ELEMENT_NODE) {
            n = getXblNextSibling(n);
        }
        return (Element) n;
    }

    /**
     * Get the last element child of a node in the fully flattened tree.
     */
    public Element getXblLastElementChild(Node n) {
        n = getXblLastChild(n);
        while (n != null && n.getNodeType() != Node.ELEMENT_NODE) {
            n = getXblPreviousSibling(n);
        }
        return (Element) n;
    }

    /**
     * Get the first element that precedes the a node in the
     * xblParentNode's xblChildNodes list.
     */
    public Element getXblPreviousElementSibling(Node n) {
        do {
            n = getXblPreviousSibling(n);
        } while (n != null && n.getNodeType() != Node.ELEMENT_NODE);
        return (Element) n;
    }

    /**
     * Get the first element that follows a node in the
     * xblParentNode's xblChildNodes list.
     */
    public Element getXblNextElementSibling(Node n) {
        do {
            n = getXblNextSibling(n);
        } while (n != null && n.getNodeType() != Node.ELEMENT_NODE);
        return (Element) n;
    }

    /**
     * Get the bound element whose shadow tree a node resides in.
     */
    public Element getXblBoundElement(Node n) {
        while (n != null && !(n instanceof XBLShadowTreeElement)) {
            XBLOMContentElement content = getXblContentElement(n);
            if (content != null) {
                n = content;
            }
            n = n.getParentNode();
        }
        if (n == null) {
            return null;
        }
        return getRecord(n).boundElement;
    }

    /**
     * Get the shadow tree of a node.
     */
    public Element getXblShadowTree(Node n) {
        if (n instanceof BindableElement) {
            BindableElement elt = (BindableElement) n;
            return elt.getShadowTree();
        }
        return null;
    }

    /**
     * Get the xbl:definition elements currently binding an element.
     */
    public NodeList getXblDefinitions(Node n) {
        final String namespaceURI = n.getNamespaceURI();
        final String localName = n.getLocalName();
        return new NodeList() {
            public Node item(int i) {
                TreeSet defs = (TreeSet) definitionLists.get(namespaceURI, localName);
                if (defs != null && defs.size() != 0 && i == 0) {
                    DefinitionRecord defRec = (DefinitionRecord) defs.first();
                    return defRec.definition;
                }
                return null;
            }
            public int getLength() {
                Set defs = (TreeSet) definitionLists.get(namespaceURI, localName);
                return defs != null && defs.size() != 0 ? 1 : 0;
            }
        };
    }

    /**
     * Returns the XBL record for the given node.
     */
    protected XBLRecord getRecord(Node n) {
        XBLManagerData xmd = (XBLManagerData) n;
        XBLRecord rec = (XBLRecord) xmd.getManagerData();
        if (rec == null) {
            rec = new XBLRecord();
            rec.node = n;
            xmd.setManagerData(rec);
        }
        return rec;
    }

    /**
     * Updates the xblPreviousSibling and xblNextSibling properties of the
     * given XBL node.
     */
    protected void updateLinks(Node n) {
        XBLRecord rec = getRecord(n);
        rec.previousSibling = null;
        rec.nextSibling = null;
        rec.linksValid = true;
        Node p = getXblParentNode(n);
        if (p != null) {
            NodeList xcn = getXblChildNodes(p);
            if (xcn instanceof XblChildNodes) {
                ((XblChildNodes) xcn).update();
            }
        }
    }

    /**
     * Returns the content element that caused the given node to be
     * present in the flattened tree.
     */
    public XBLOMContentElement getXblContentElement(Node n) {
        return getRecord(n).contentElement;
    }

    /**
     * Determines the number of nodes events should bubble if the
     * mouse pointer has moved from one element to another.
     * @param from the element from which the mouse pointer moved
     * @param to   the element to which the mouse pointer moved
     */
    public static int computeBubbleLimit(Node from, Node to) {
        ArrayList fromList = new ArrayList(10);
        ArrayList toList = new ArrayList(10);
        while (from != null) {
            fromList.add(from);
            from = ((NodeXBL) from).getXblParentNode();
        }
        while (to != null) {
            toList.add(to);
            to = ((NodeXBL) to).getXblParentNode();
        }
        int fromSize = fromList.size();
        int toSize = toList.size();
        for (int i = 0; i < fromSize && i < toSize; i++) {
            Node n1 = (Node) fromList.get(fromSize - i - 1);
            Node n2 = (Node) toList.get(toSize - i - 1);
            if (n1 != n2) {
                Node prevBoundElement = ((NodeXBL) n1).getXblBoundElement();
                while (i > 0 && prevBoundElement != fromList.get(fromSize - i - 1)) {
                    i--;
                }
                return fromSize - i - 1;
            }
        }
        return 1;
    }

    /**
     * Returns the ContentManager that handles the shadow tree the given
     * node resides in.
     */
    public ContentManager getContentManager(Node n) {
        Node b = getXblBoundElement(n);
        if (b != null) {
            Element s = getXblShadowTree(b);
            if (s != null) {
                ContentManager cm;
                Document doc = b.getOwnerDocument();
                if (doc != document) {
                    DefaultXBLManager xm = (DefaultXBLManager)
                        ((AbstractDocument) doc).getXBLManager();
                    cm = (ContentManager) xm.contentManagers.get(s);
                } else {
                    cm = (ContentManager) contentManagers.get(s);
                }
                return cm;
            }
        }
        return null;
    }

    /**
     * Records the ContentManager that handles the given shadow tree.
     */
    void setContentManager(Element shadow, ContentManager cm) {
        if (cm == null) {
            contentManagers.remove(shadow);
        } else {
            contentManagers.put(shadow, cm);
        }
    }

    /**
     * Mark the xblChildNodes and xblScopedChildNodes variables
     * as invalid.
     */
    public void invalidateChildNodes(Node n) {
        XBLRecord rec = getRecord(n);
        if (rec.childNodes != null) {
            rec.childNodes.invalidate();
        }
        if (rec.scopedChildNodes != null) {
            rec.scopedChildNodes.invalidate();
        }
    }

    /**
     * Adds the specified ContentSelectionChangedListener to the
     * global listener list.
     */
    public void addContentSelectionChangedListener
            (ContentSelectionChangedListener l) {
        contentSelectionChangedListenerList.add
            (ContentSelectionChangedListener.class, l);
    }

    /**
     * Removes the specified ContentSelectionChangedListener from the
     * global listener list.
     */
    public void removeContentSelectionChangedListener
            (ContentSelectionChangedListener l) {
        contentSelectionChangedListenerList.remove
            (ContentSelectionChangedListener.class, l);
    }

    /**
     * Returns an array of the gloabl ContentSelectionChangedListeners.
     */
    protected Object[] getContentSelectionChangedListeners() {
        return contentSelectionChangedListenerList.getListenerList();
    }

    /**
     * Called by the ContentManager of a shadow tree to indicate some
     * selected nodes have changed.
     */
    void shadowTreeSelectedContentChanged(Set deselected, Set selected) {
        Iterator i = deselected.iterator();
        while (i.hasNext()) {
            Node n = (Node) i.next();
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                unbind((Element) n);
            }
        }
        i = selected.iterator();
        while (i.hasNext()) {
            Node n = (Node) i.next();
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                bind((Element) n);
            }
        }
    }

    /**
     * Adds the specified BindingListener to the global listener list.
     */
    public void addBindingListener(BindingListener l) {
        bindingListenerList.add(BindingListener.class, l);
    }

    /**
     * Removes the specified BindingListener from the global listener list.
     */
    public void removeBindingListener(BindingListener l) {
        bindingListenerList.remove(BindingListener.class, l);
    }

    /**
     * Dispatches a BindingEvent the registered listeners.
     * @param bindableElement the bindable element whose binding has changed
     * @param shadowTree the new shadow tree of the bindable element
     */
    protected void dispatchBindingChangedEvent(Element bindableElement,
                                               Element shadowTree) {
        Object[] ls = bindingListenerList.getListenerList();
        for (int i = ls.length - 2; i >= 0; i -= 2) {
            BindingListener l = (BindingListener) ls[i + 1];
            l.bindingChanged(bindableElement, shadowTree);
        }
    }

    /**
     * Returns whether the given definition element is the active one
     * for its element name.
     */
    protected boolean isActiveDefinition(XBLOMDefinitionElement def,
                                         Element imp) {
        DefinitionRecord defRec = (DefinitionRecord) definitions.get(def, imp);
        if (defRec == null) {
            return false;
        }
        return defRec == getActiveDefinition(defRec.namespaceURI,
                                             defRec.localName);
    }

    /**
     * Record class for storing information about an XBL definition.
     */
    protected class DefinitionRecord implements Comparable {

        /**
         * The namespace URI.
         */
        public String namespaceURI;

        /**
         * The local name.
         */
        public String localName;

        /**
         * The definition element.
         */
        public XBLOMDefinitionElement definition;

        /**
         * The template element for this definition.
         */
        public XBLOMTemplateElement template;

        /**
         * The import element that imported this definition.
         */
        public Element importElement;

        /**
         * Creates a new DefinitionRecord.
         */
        public DefinitionRecord(String ns,
                                String ln,
                                XBLOMDefinitionElement def,
                                XBLOMTemplateElement t,
                                Element imp) {
            namespaceURI = ns;
            localName = ln;
            definition = def;
            template = t;
            importElement = imp;
        }

        /**
         * Returns whether two definition records are the same.
         */
        public boolean equals(Object other) {
            return compareTo(other) == 0;
        }

        /**
         * Compares two definition records.
         */
        public int compareTo(Object other) {
            DefinitionRecord rec = (DefinitionRecord) other;
            AbstractNode n1, n2;
            if (importElement == null) {
                n1 = definition;
                if (rec.importElement == null) {
                    n2 = rec.definition;
                } else {
                    n2 = (AbstractNode) rec.importElement;
                }
            } else if (rec.importElement == null) {
                n1 = (AbstractNode) importElement;
                n2 = rec.definition;
            } else if (definition.getOwnerDocument()
                        == rec.definition.getOwnerDocument()) {
                n1 = definition;
                n2 = rec.definition;
            } else {
                n1 = (AbstractNode) importElement;
                n2 = (AbstractNode) rec.importElement;
            }
            short comp = n1.compareDocumentPosition(n2);
            if ((comp & AbstractNode.DOCUMENT_POSITION_PRECEDING) != 0) {
                return -1;
            }
            if ((comp & AbstractNode.DOCUMENT_POSITION_FOLLOWING) != 0) {
                return 1;
            }
            return 0;
        }
    }

    /**
     * Record class for storing information about an XBL import.
     */
    protected class ImportRecord {

        /**
         * The import element.
         */
        public Element importElement;

        /**
         * The imported tree.
         */
        public Node node;

        /**
         * The DOM node inserted listener for definitions accessed through
         * this import.
         */
        public DefNodeInsertedListener defNodeInsertedListener;

        /**
         * The DOM node removed listener for definitions accessed through
         * this import.
         */
        public DefNodeRemovedListener defNodeRemovedListener;

        /**
         * The DOM attribute mutation listener for definitions accessed through
         * this import.
         */
        public DefAttrListener defAttrListener;

        /**
         * The DOM node inserted listener for the imported tree.
         */
        public ImportInsertedListener importInsertedListener;

        /**
         * The DOM node removed listener for the imported tree.
         */
        public ImportRemovedListener importRemovedListener;

        /**
         * The DOM subtree modified listener for the imported tree.
         */
        public ImportSubtreeListener importSubtreeListener;

        /**
         * The DOM subtree modified listener for templates of definitions
         * accessed through this import.
         */
        public TemplateMutationListener templateMutationListener;

        /**
         * Creates a new ImportRecord.
         */
        public ImportRecord(Element imp, Node n) {
            importElement = imp;
            node = n;
            defNodeInsertedListener = new DefNodeInsertedListener(imp);
            defNodeRemovedListener = new DefNodeRemovedListener(imp);
            defAttrListener = new DefAttrListener(imp);
            importInsertedListener = new ImportInsertedListener(imp);
            importRemovedListener = new ImportRemovedListener();
            importSubtreeListener
                = new ImportSubtreeListener(imp, importRemovedListener);
            templateMutationListener = new TemplateMutationListener(imp);
        }
    }

    /**
     * DOM node inserted listener for imported XBL trees.
     */
    protected class ImportInsertedListener implements EventListener {

        /**
         * The import element.
         */
        protected Element importElement;

        /**
         * Creates a new ImportInsertedListener.
         */
        public ImportInsertedListener(Element importElement) {
            this.importElement = importElement;
        }

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            EventTarget target = evt.getTarget();
            if (target instanceof XBLOMDefinitionElement) {
                XBLOMDefinitionElement def = (XBLOMDefinitionElement) target;
                addDefinition(def.getElementNamespaceURI(),
                              def.getElementLocalName(),
                              def,
                              importElement);
            }
        }
    }

    /**
     * DOM node removed listener for imported XBL trees.
     */
    protected class ImportRemovedListener implements EventListener {

        /**
         * List of definition elements to be removed from the document.
         */
        protected LinkedList toBeRemoved = new LinkedList();

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            toBeRemoved.add(evt.getTarget());
        }
    }

    /**
     * DOM subtree listener for imported XBL trees.
     */
    protected class ImportSubtreeListener implements EventListener {

        /**
         * The import element.
         */
        protected Element importElement;

        /**
         * The ImportedRemovedListener to check for to-be-removed definitions.
         */
        protected ImportRemovedListener importRemovedListener;

        /**
         * Creates a new ImportSubtreeListener.
         */
        public ImportSubtreeListener(Element imp,
                                     ImportRemovedListener irl) {
            importElement = imp;
            importRemovedListener = irl;
        }

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            Object[] defs = importRemovedListener.toBeRemoved.toArray();
            importRemovedListener.toBeRemoved.clear();
            for (int i = 0; i < defs.length; i++) {
                XBLOMDefinitionElement def = (XBLOMDefinitionElement) defs[i];
                DefinitionRecord defRec
                    = (DefinitionRecord) definitions.get(def, importElement);
                removeDefinition(defRec);
            }
        }
    }

    /**
     * DOM node inserted listener for the document.
     */
    protected class DocInsertedListener implements EventListener {

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            EventTarget target = evt.getTarget();
            if (target instanceof XBLOMDefinitionElement) {
                // only handle definition elements in document-level scope
                if (getXblBoundElement((Node) target) == null) {       // ??? suspect cast ???
                    XBLOMDefinitionElement def
                        = (XBLOMDefinitionElement) target;
                    if (def.getAttributeNS(null, XBL_REF_ATTRIBUTE).length()
                            == 0) {
                        addDefinition(def.getElementNamespaceURI(),
                                      def.getElementLocalName(),
                                      def,
                                      null);
                    } else {
                        addDefinitionRef(def);
                    }
                }
            } else if (target instanceof XBLOMImportElement) {
                // only handle import elements in document-level scope
                if (getXblBoundElement((Node) target) == null) {      // ??? suspect cast ???
                    addImport((Element) target);
                }
            } else {
                evt = XBLEventSupport.getUltimateOriginalEvent(evt);
                target = evt.getTarget();
                Node parent = getXblParentNode((Node) target);
                if (parent != null) {
                    invalidateChildNodes(parent);
                }
                if (target instanceof BindableElement) {
                    // Only bind it if it's not the descendent of a bound
                    // element.  If it is, and this new element will be
                    // selected by an xbl:content element in the shadow tree,
                    // the ContentManager will bind it.
                    for (Node n = ((Node) target).getParentNode();
                            n != null;
                            n = n.getParentNode()) {
                        if (n instanceof BindableElement
                                && getRecord(n).definitionElement != null) {
                            return;
                        }
                    }
                    bind((Element) target);
                }
            }
        }
    }

    /**
     * DOM node removed listener for the document.
     */
    protected class DocRemovedListener implements EventListener {

        /**
         * List of definition elements to be removed from the document.
         */
        protected LinkedList defsToBeRemoved = new LinkedList();

        /**
         * List of import elements to be removed from the document.
         */
        protected LinkedList importsToBeRemoved = new LinkedList();

        /**
         * List of nodes to have their XBL child lists invalidated.
         */
        protected LinkedList nodesToBeInvalidated = new LinkedList();

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            EventTarget target = evt.getTarget();
            if (target instanceof XBLOMDefinitionElement) {
                // only handle definition elements in document-level scope
                if (getXblBoundElement((Node) target) == null) {
                    defsToBeRemoved.add(target);
                }
            } else if (target instanceof XBLOMImportElement) {
                // only handle import elements in document-level scope
                if (getXblBoundElement((Node) target) == null) {
                    importsToBeRemoved.add(target);
                }
            }

            Node parent = getXblParentNode((Node) target);
            if (parent != null) {
                nodesToBeInvalidated.add(parent);
            }
        }
    }

    /**
     * DOM subtree mutation listener for the document.
     */
    protected class DocSubtreeListener implements EventListener {

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            Object[] defs = docRemovedListener.defsToBeRemoved.toArray();
            docRemovedListener.defsToBeRemoved.clear();
            for (int i = 0; i < defs.length; i++) {
                XBLOMDefinitionElement def = (XBLOMDefinitionElement) defs[i];
                if (def.getAttributeNS(null, XBL_REF_ATTRIBUTE).length() == 0) {
                    DefinitionRecord defRec
                        = (DefinitionRecord) definitions.get(def, null);
                    removeDefinition(defRec);
                } else {
                    removeDefinitionRef(def);
                }
            }

            Object[] imps = docRemovedListener.importsToBeRemoved.toArray();
            docRemovedListener.importsToBeRemoved.clear();
            for (int i = 0; i < imps.length; i++) {
                removeImport((Element) imps[i]);
            }

            Object[] nodes = docRemovedListener.nodesToBeInvalidated.toArray();
            docRemovedListener.nodesToBeInvalidated.clear();
            for (int i = 0; i < nodes.length; i++) {
                invalidateChildNodes((Node) nodes[i]);
            }
        }
    }

    /**
     * DOM mutation listener for template elements.
     */
    protected class TemplateMutationListener implements EventListener {

        /**
         * The import element.
         */
        protected Element importElement;

        /**
         * Creates a new TemplateMutationListener.
         */
        public TemplateMutationListener(Element imp) {
            importElement = imp;
        }

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            Node n = (Node) evt.getTarget();
            while (n != null && !(n instanceof XBLOMDefinitionElement)) {
                n = n.getParentNode();
            }

            DefinitionRecord defRec
                = (DefinitionRecord) definitions.get(n, importElement);
            if (defRec == null) {
                return;
            }

            rebind(defRec.namespaceURI, defRec.localName,
                   document.getDocumentElement());
        }
    }

    /**
     * DOM attribute mutation listener for definition elements.
     */
    protected class DefAttrListener implements EventListener {

        /**
         * The import element.
         */
        protected Element importElement;

        /**
         * Creates a new DefAttrListener.
         */
        public DefAttrListener(Element imp) {
            importElement = imp;
        }

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            EventTarget target = evt.getTarget();
            if (!(target instanceof XBLOMDefinitionElement)) {
                return;
            }

            XBLOMDefinitionElement def = (XBLOMDefinitionElement) target;
            if (!isActiveDefinition(def, importElement)) {
                return;
            }

            MutationEvent mevt = (MutationEvent) evt;
            String attrName = mevt.getAttrName();
            if (attrName.equals(XBL_ELEMENT_ATTRIBUTE)) {
                DefinitionRecord defRec =
                    (DefinitionRecord) definitions.get(def, importElement);
                removeDefinition(defRec);

                addDefinition(def.getElementNamespaceURI(),
                              def.getElementLocalName(),
                              def,
                              importElement);
            } else if (attrName.equals(XBL_REF_ATTRIBUTE)) {
                if (mevt.getNewValue().length() != 0) {
                    DefinitionRecord defRec =
                        (DefinitionRecord) definitions.get(def, importElement);
                    removeDefinition(defRec);
                    addDefinitionRef(def);
                }
            }
        }
    }

    /**
     * DOM node inserted listener for definition elements.
     */
    protected class DefNodeInsertedListener implements EventListener {

        /**
         * The import element.
         */
        protected Element importElement;

        /**
         * Creates a new DefNodeInsertedListener.
         */
        public DefNodeInsertedListener(Element imp) {
            importElement = imp;
        }

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            MutationEvent mevt = (MutationEvent) evt;
            Node parent = mevt.getRelatedNode();
            if (!(parent instanceof XBLOMDefinitionElement)) {
                return;
            }

            EventTarget target = evt.getTarget();
            if (!(target instanceof XBLOMTemplateElement)) {
                return;
            }
            XBLOMTemplateElement template = (XBLOMTemplateElement) target;

            DefinitionRecord defRec
                = (DefinitionRecord) definitions.get(parent, importElement);
            if (defRec == null) {
                return;
            }

            ImportRecord ir = (ImportRecord) imports.get(importElement);

            if (defRec.template != null) {
                for (Node n = parent.getFirstChild();
                        n != null;
                        n = n.getNextSibling()) {
                    if (n == template) {
                        removeTemplateElementListeners(defRec.template, ir);
                        defRec.template = template;
                        break;
                    } else if (n == defRec.template) {
                        return;
                    }
                }
            } else {
                defRec.template = template;
            }
            addTemplateElementListeners(template, ir);
            rebind(defRec.namespaceURI, defRec.localName,
                   document.getDocumentElement());
        }
    }

    /**
     * DOM node removed listener for definition elements.
     */
    protected class DefNodeRemovedListener implements EventListener {

        /**
         * The import element.
         */
        protected Element importElement;

        /**
         * Creates a new DefNodeRemovedListener.
         */
        public DefNodeRemovedListener(Element imp) {
            importElement = imp;
        }

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            MutationEvent mevt = (MutationEvent) evt;
            Node parent = mevt.getRelatedNode();
            if (!(parent instanceof XBLOMDefinitionElement)) {
                return;
            }

            EventTarget target = evt.getTarget();
            if (!(target instanceof XBLOMTemplateElement)) {
                return;
            }
            XBLOMTemplateElement template = (XBLOMTemplateElement) target;

            DefinitionRecord defRec
                = (DefinitionRecord) definitions.get(parent, importElement);
            if (defRec == null || defRec.template != template) {
                return;
            }

            ImportRecord ir = (ImportRecord) imports.get(importElement);

            removeTemplateElementListeners(template, ir);
            defRec.template = null;

            for (Node n = template.getNextSibling();
                    n != null;
                    n = n.getNextSibling()) {
                if (n instanceof XBLOMTemplateElement) {
                    defRec.template = (XBLOMTemplateElement) n;
                    break;
                }
            }

            addTemplateElementListeners(defRec.template, ir);
            rebind(defRec.namespaceURI, defRec.localName,
                   document.getDocumentElement());
        }
    }

    /**
     * DOM attribute mutation listener for import elements.
     */
    protected class ImportAttrListener implements EventListener {

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            EventTarget target = evt.getTarget();
            if (target != evt.getCurrentTarget()) {
                return;
            }

            MutationEvent mevt = (MutationEvent) evt;
            if (mevt.getAttrName().equals(XBL_BINDINGS_ATTRIBUTE)) {
                Element imp = (Element) target;
                removeImport(imp);
                addImport(imp);
            }
        }
    }

    /**
     * DOM attribute mutation listener for referencing definition elements.
     */
    protected class RefAttrListener implements EventListener {

        /**
         * Handles the event.
         */
        public void handleEvent(Event evt) {
            EventTarget target = evt.getTarget();
            if (target != evt.getCurrentTarget()) {
                return;
            }

            MutationEvent mevt = (MutationEvent) evt;
            if (mevt.getAttrName().equals(XBL_REF_ATTRIBUTE)) {
                Element defRef = (Element) target;
                removeDefinitionRef(defRef);
                if (mevt.getNewValue().length() == 0) {
                    XBLOMDefinitionElement def
                        = (XBLOMDefinitionElement) defRef;
                    String ns = def.getElementNamespaceURI();
                    String ln = def.getElementLocalName();
                    addDefinition(ns, ln,
                                  (XBLOMDefinitionElement) defRef, null);
                } else {
                    addDefinitionRef(defRef);
                }
            }
        }
    }

    /**
     * XBL record.
     */
    protected class XBLRecord {

        /**
         * The node.
         */
        public Node node;

        /**
         * The xblChildNodes NodeList for this node.
         */
        public XblChildNodes childNodes;

        /**
         * The xblScopedChildNodes NodeList for this node.
         */
        public XblScopedChildNodes scopedChildNodes;

        /**
         * The content element which caused this node to appear in the
         * flattened tree.
         */
        public XBLOMContentElement contentElement;

        /**
         * The definition element that applies to this element.
         */
        public XBLOMDefinitionElement definitionElement;

        /**
         * The bound element that owns this shadow tree, if this node
         * is an XBLOMShadowTreeElement.
         */
        public BindableElement boundElement;

        /**
         * Whether the next/previous links are valid.
         */
        public boolean linksValid;

        /**
         * The following sibling in the flattened tree.
         */
        public Node nextSibling;

        /**
         * The previous sibling in the flattened tree.
         */
        public Node previousSibling;
    }

    /**
     * To iterate over the XBL child nodes.
     */
    protected class XblChildNodes implements NodeList {

        /**
         * The XBLRecord.
         */
        protected XBLRecord record;

        /**
         * The nodes.
         */
        protected List nodes;

        /**
         * The number of nodes.
         */
        protected int size;

        /**
         * Creates a new XblChildNodes.
         */
        public XblChildNodes(XBLRecord rec) {
            record = rec;
            nodes = new ArrayList();
            size = -1;
        }

        /**
         * Update the NodeList.
         */
        protected void update() {
            size = 0;
            Node shadowTree = getXblShadowTree(record.node);
            Node last = null;
            Node m = shadowTree == null ? record.node.getFirstChild()
                                        : shadowTree.getFirstChild();
            while (m != null) {
                last = collectXblChildNodes(m, last);
                m = m.getNextSibling();
            }
            if (last != null) {
                XBLRecord rec = getRecord(last);
                rec.nextSibling = null;
                rec.linksValid = true;
            }
        }

        /**
         * Find the XBL child nodes of this element.
         */
        protected Node collectXblChildNodes(Node n, Node prev) {
            boolean isChild = false;
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                if (!XBL_NAMESPACE_URI.equals(n.getNamespaceURI())) {
                    isChild = true;
                } else if (n instanceof XBLOMContentElement) {
                    ContentManager cm = getContentManager(n);
                    if (cm != null) {
                        NodeList selected =
                            cm.getSelectedContent((XBLOMContentElement) n);
                        for (int i = 0; i < selected.getLength(); i++) {
                            prev = collectXblChildNodes(selected.item(i),
                                                        prev);
                        }
                    }
                }
            } else {
                isChild = true;
            }
            if (isChild) {
                nodes.add(n);
                size++;
                if (prev != null) {
                    XBLRecord rec = getRecord(prev);
                    rec.nextSibling = n;
                    rec.linksValid = true;
                }
                XBLRecord rec = getRecord(n);
                rec.previousSibling = prev;
                rec.linksValid = true;
                prev = n;
            }
            return prev;
        }

        /**
         * Mark the xblNextSibling and xblPreviousSibling variables
         * on each node in the list as invalid, then invalidate the
         * NodeList.
         */
        public void invalidate() {
            for (int i = 0; i < size; i++) {
                XBLRecord rec = getRecord((Node) nodes.get(i));
                rec.previousSibling = null;
                rec.nextSibling = null;
                rec.linksValid = false;
            }
            nodes.clear();
            size = -1;
        }

        /**
         * Returns the first node in the list.
         */
        public Node getFirstNode() {
            if (size == -1) {
                update();
            }
            return size == 0 ? null : (Node) nodes.get(0);
        }

        /**
         * Returns the last node in the list.
         */
        public Node getLastNode() {
            if (size == -1) {
                update();
            }
            return size == 0 ? null : (Node) nodes.get( nodes.size() -1 );
        }

        /**
         * <b>DOM</b>: Implements {@link org.w3c.dom.NodeList#item(int)}.
         */
        public Node item(int index) {
            if (size == -1) {
                update();
            }
            if (index < 0 || index >= size) {
                return null;
            }
            return (Node) nodes.get(index);
        }

        /**
         * <b>DOM</b>: Implements {@link org.w3c.dom.NodeList#getLength()}.
         */
        public int getLength() {
            if (size == -1) {
                update();
            }
            return size;
        }
    }

    /**
     * To iterate over the scoped XBL child nodes.
     */
    protected class XblScopedChildNodes extends XblChildNodes {

        /**
         * Creates a new XblScopedChildNodes object.
         */
        public XblScopedChildNodes(XBLRecord rec) {
            super(rec);
        }

        /**
         * Update the NodeList.
         */
        protected void update() {
            size = 0;
            Node shadowTree = getXblShadowTree(record.node);
            Node n = shadowTree == null ? record.node.getFirstChild()
                                        : shadowTree.getFirstChild();
            while (n != null) {
                collectXblScopedChildNodes(n);
                n = n.getNextSibling();
            }
        }

        /**
         * Find the XBL child nodes of this element.
         */
        protected void collectXblScopedChildNodes(Node n) {
            boolean isChild = false;
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                if (!n.getNamespaceURI().equals(XBL_NAMESPACE_URI)) {
                    isChild = true;
                } else if (n instanceof XBLOMContentElement) {
                    ContentManager cm = getContentManager(n);
                    if (cm != null) {
                        NodeList selected =
                            cm.getSelectedContent((XBLOMContentElement) n);
                        for (int i = 0; i < selected.getLength(); i++) {
                            collectXblScopedChildNodes(selected.item(i));
                        }
                    }
                }
            } else {
                isChild = true;
            }
            if (isChild) {
                nodes.add(n);
                size++;
            }
        }
    }
}
