/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

package org.apache.batik.bridge;

import java.awt.Cursor;
import java.awt.geom.Dimension2D;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.batik.css.engine.CSSContext;
import org.apache.batik.css.engine.CSSEngine;
import org.apache.batik.css.engine.CSSEngineEvent;
import org.apache.batik.css.engine.CSSEngineListener;
import org.apache.batik.css.engine.CSSEngineUserAgent;
import org.apache.batik.css.engine.SVGCSSEngine;
import org.apache.batik.css.engine.SystemColorSupport;
import org.apache.batik.css.engine.value.Value;
import org.apache.batik.dom.svg.SVGContext;
import org.apache.batik.dom.svg.SVGDOMImplementation;
import org.apache.batik.dom.svg.SVGOMDocument;
import org.apache.batik.dom.svg.SVGOMElement;
import org.apache.batik.dom.svg.SVGStylableElement;
import org.apache.batik.gvt.CompositeGraphicsNode;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.gvt.TextPainter;
import org.apache.batik.script.Interpreter;
import org.apache.batik.script.InterpreterPool;
import org.apache.batik.util.CSSConstants;
import org.apache.batik.util.CleanerThread;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.util.Service;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.events.MouseEvent;
import org.w3c.dom.events.MutationEvent;
import org.w3c.dom.svg.SVGDocument;

/**
 * This class represents a context used by the various bridges and the
 * builder. A bridge context is associated to a particular document
 * and cannot be reused.
 *
 * The context encapsulates the dynamic bindings between DOM elements
 * and GVT nodes, graphic contexts such as a <tt>GraphicsNodeRenderContext</tt>,
 * and the different objects required by the GVT builder to interpret
 * a SVG DOM tree such as the current viewport or the user agent.
 *
 * @author <a href="mailto:Thierry.Kormann@sophia.inria.fr">Thierry Kormann</a>
 * @version $Id: BridgeContext.java,v 1.74 2003/12/14 15:14:57 deweese Exp $
 */
public class BridgeContext implements ErrorConstants, CSSContext {

    /**
     * The document is bridge context is dedicated to.
     */
    protected Document document;

    /**
     * The GVT builder that might be used to create a GVT subtree.
     */
    protected GVTBuilder gvtBuilder;

    /**
     * The interpreter cache per document.
     * key is the language -
     * value is a Interpreter
     */
    protected Map interpreterMap = new HashMap(7);

    /**
     * A Map of all the font families already matched. This is
     * to reduce the number of instances of GVTFontFamilies and to
     * hopefully reduce the time taken to search for a matching SVG font.
     */
    private Map fontFamilyMap;

    /**
     * The viewports.
     * key is an Element -
     * value is a Viewport
     */
    protected Map viewportMap = new WeakHashMap();

    /**
     * The viewport stack. Used in building time.
     */
    protected List viewportStack = new LinkedList();

    /**
     * The user agent.
     */
    protected UserAgent userAgent;

    /**
     * Binding Map:
     * key is an SVG Element -
     * value is a GraphicsNode
     */
    protected Map elementNodeMap;

    /**
     * Binding Map:
     * key is GraphicsNode -
     * value is a SVG Element.
     */
    protected Map nodeElementMap;

    /**
     * Bridge Map:
     * Keys are namespace URI - values are HashMap (with keys are local
     * name and values are a Bridge instance).
     */
    protected Map namespaceURIMap;

    /**
     * Element Data Map:
     * This is a general location for elements to 'cache'
     * data.  Such as the graphics tree for a pattern or
     * the Gradient arrays.
     *
     * This is a weak hash map and the data is referenced
     * by SoftReference so both must be referenced elsewhere
     * to stay live.
     */
    protected Map elementDataMap;


    /**
     * The interpreter pool used to handle scripts.
     */
    protected InterpreterPool interpreterPool;

    /**
     * The document loader used to load/create Document.
     */
    protected DocumentLoader documentLoader;

    /**
     * The size of the document.
     */
    protected Dimension2D documentSize;

    /**
     * The text painter to use. Typically, you can specify the text painter that
     * will be used be text nodes.
     */
    protected TextPainter textPainter;

    /**
     * Indicates that no DOM listeners should be registered.
     * In this case the generated GVT tree should be totally
     * independent of the DOM tree.
     */
    public final static int STATIC      = 0;
    /**
     * Indicates that DOM listeners should be registered to support,
     * 'interactivity' this includes anchors and cursors, but does not
     * include support for DOM modifications.
     */
    public final static int INTERACTIVE = 1;

    /**
     * Indicates that all DOM listeners should be registered. This supports
     * 'interactivity' (anchors and cursors), as well as DOM modifications
     * listeners to update the GVT rendering tree.
     */
    public final static int DYNAMIC     = 2;

    /**
     * Whether the bridge should support dynamic, or interactive features.
     */
    protected int dynamicStatus = STATIC;

    /**
     * The update manager.
     */
    protected UpdateManager updateManager;

    /**
     * Constructs a new empty bridge context.
     */
    protected BridgeContext() {}

    /**
     * By default we share a unique instance of InterpreterPool.
     */
    private static InterpreterPool sharedPool = new InterpreterPool();

    /**
     * Constructs a new bridge context.
     * @param userAgent the user agent
     */
    public BridgeContext(UserAgent userAgent) {
        this(userAgent,
             sharedPool,
             new DocumentLoader(userAgent));
    }

    /**
     * Constructs a new bridge context.
     * @param userAgent the user agent
     * @param documentLoader document loader
     */
    public BridgeContext(UserAgent userAgent,
                         DocumentLoader loader) {
        this(userAgent, sharedPool, loader);
    }

    /**
     * Constructs a new bridge context.
     * @param userAgent the user agent
     * @param interpreterPool the interpreter pool
     * @param documentLoader document loader
     */
    public BridgeContext(UserAgent userAgent,
                         InterpreterPool interpreterPool,
                         DocumentLoader documentLoader) {
        this.userAgent = userAgent;
        this.viewportMap.put(userAgent, new UserAgentViewport(userAgent));
        this.interpreterPool = interpreterPool;
        this.documentLoader = documentLoader;
        documentLoader.setBridgeContext(this);
        registerSVGBridges(this);
    }

    /**
     * Initializes the given document.
     */
    protected void initializeDocument(Document document) {
        SVGOMDocument doc = (SVGOMDocument)document;
        CSSEngine eng = doc.getCSSEngine();
        if (eng == null) {
            SVGDOMImplementation impl;
            impl = (SVGDOMImplementation)doc.getImplementation();
            eng = impl.createCSSEngine(doc, this);
            eng.setCSSEngineUserAgent(new CSSEngineUserAgentWrapper(userAgent));
            doc.setCSSEngine(eng);
            eng.setMedia(userAgent.getMedia());
            String uri = userAgent.getUserStyleSheetURI();
            if (uri != null) {
                try {
                    URL url = new URL(uri);
                    eng.setUserAgentStyleSheet
                        (eng.parseStyleSheet(url, "all"));
                } catch (MalformedURLException e) {
                    userAgent.displayError(e);
                }
            }
            eng.setAlternateStyleSheet(userAgent.getAlternateStyleSheet());
        }
    }

    // properties ////////////////////////////////////////////////////////////

    /**
     * Sets the text painter that will be used by text nodes. This attributes
     * might be used by bridges (especially SVGTextElementBridge) to set the
     * text painter of each TextNode.
     *
     * @param textPainter the text painter for text nodes 
     */
    public void setTextPainter(TextPainter textPainter) {
	this.textPainter = textPainter;
    }

    /**
     * Returns the text painter that will be used be text nodes.
     */
    public TextPainter getTextPainter() {
	return textPainter;
    }

    /**
     * Returns the document this bridge context is dedicated to.
     */
    public Document getDocument() {
        return document;
    }

    /**
     * Sets the document this bridge context is dedicated to, to the
     * specified document.
     * @param document the document
     */
    protected void setDocument(Document document) {
        if (this.document != document){
            fontFamilyMap = null;
        }
        this.document = document;
    }

    /**
     * Returns the map of font families
     */
    public Map getFontFamilyMap(){
        if (fontFamilyMap == null){
            fontFamilyMap = new HashMap();
        }
        return fontFamilyMap;
    }

    /**
     * Sets the map of font families to the specified value.
     *
     *@param fontFamilyMap the map of font families
     */
    protected void setFontFamilyMap(Map fontFamilyMap) {
        this.fontFamilyMap = fontFamilyMap;
    }

    /**
     * Set Element Data.
     * Associates data object with element so it can be
     * retrieved later.
     */
    public void setElementData(Node n, Object data) {
        if (elementDataMap == null)
            elementDataMap = new WeakHashMap();
        elementDataMap.put(n, new SoftReference(data));
    }

    /**
     * Set Element Data.
     * Associates data object with element so it can be
     * retrieved later.
     */
    public Object getElementData(Node n) {
        if (elementDataMap == null)
            return null;
        Object o = elementDataMap.get(n);
        if (o == null) return null;
        SoftReference sr = (SoftReference)o;
        o = sr.get();
        if (o == null) {
            elementDataMap.remove(n);
        }
        return o;
    }

    /**
     * Returns the user agent of this bridge context.
     */
    public UserAgent getUserAgent() {
        return userAgent;
    }

    /**
     * Sets the user agent to the specified user agent.
     * @param userAgent the user agent
     */
    protected void setUserAgent(UserAgent userAgent) {
        this.userAgent = userAgent;
    }

    /**
     * Returns the GVT builder that is currently used to build the GVT tree.
     */
    public GVTBuilder getGVTBuilder() {
        return gvtBuilder;
    }

    /**
     * Sets the GVT builder that uses this context.
     */
    protected void setGVTBuilder(GVTBuilder gvtBuilder) {
        this.gvtBuilder = gvtBuilder;
    }

    /**
     * Returns the interpreter pool used to handle scripts.
     */
    public InterpreterPool getInterpreterPool() {
        return interpreterPool;
    }

    /**
     * Returns the focus manager.
     */
    public FocusManager getFocusManager() {
        return focusManager;
    }

    /**
     * Returns the cursor manager
     */
    public CursorManager getCursorManager() {
        return cursorManager;
    }

    /**
     * Sets the interpreter pool used to handle scripts to the
     * specified interpreter pool.
     * @param interpreterPool the interpreter pool
     */
    protected void setInterpreterPool(InterpreterPool interpreterPool) {
        this.interpreterPool = interpreterPool;
    }

    /**
     * Returns a Interpreter for the specified language.
     *
     * @param language the scripting language
     */
    public Interpreter getInterpreter(String language) {
        if (document == null) {
            throw new RuntimeException("Unknown document");
        }
        Interpreter interpreter = (Interpreter)interpreterMap.get(language);
        if (interpreter == null) {
            interpreter = interpreterPool.createInterpreter(document, language);
            interpreterMap.put(language, interpreter);
        }
        return interpreter;
    }

    /**
     * Returns the document loader used to load external documents.
     */
    public DocumentLoader getDocumentLoader() {
        return documentLoader;
    }

    /**
     * Sets the document loader used to load external documents.
     * @param newDocumentLoader the new document loader
     */
    protected void setDocumentLoader(DocumentLoader newDocumentLoader) {
        this.documentLoader = newDocumentLoader;
    }

    /**
     * Returns the actual size of the document or null if the document
     * has not been built yet.
     */
    public Dimension2D getDocumentSize() {
        return documentSize;
    }

    /**
     * Sets the size of the document to the specified dimension.
     *
     * @param d the actual size of the SVG document
     */
    protected void setDocumentSize(Dimension2D d) {
        this.documentSize = d;
    }

    /**
     * Returns true if the document is dynamic, false otherwise.
     */
    public boolean isDynamic() {
        return (dynamicStatus == DYNAMIC);
    }

    /**
     * Returns true if the document is interactive, false otherwise.
     */
    public boolean isInteractive() {
        return (dynamicStatus != STATIC);
    }

    /**
     * Sets the document as a STATIC, INTERACTIVE or DYNAMIC document. 
     * Call this method before the build phase 
     * (ie. before <tt>gvtBuilder.build(...)</tt>)
     * otherwise, that will have no effect.
     *
     *@param status the document dynamicStatus
     */
    public void setDynamicState(int status) {
        dynamicStatus = status;
    }

    /**
     * Sets the document as DYNAMIC if <tt>dynamic</tt> is true
     * STATIC otherwise.
     */
    public void setDynamic(boolean dynamic) {
        if (dynamic)
            setDynamicState(DYNAMIC);
        else
            setDynamicState(STATIC);
    }

    /**
     * Sets the document as INTERACTIVE if <tt>interactive</tt> is
     * true STATIC otherwise.
     */
    public void setInteractive(boolean interactive) {
        if (interactive)
            setDynamicState(INTERACTIVE);
        else
            setDynamicState(STATIC);
    }


    /**
     * Returns the update manager, if the bridge supports dynamic features.
     */
    public UpdateManager getUpdateManager() {
        return updateManager;
    }

    /**
     * Sets the update manager.
     */
    protected void setUpdateManager(UpdateManager um) {
        updateManager = um;
    }

    // reference management //////////////////////////////////////////////////

    /**
     * Returns the element referenced by the specified element by the
     * specified uri. The referenced element can not be a Document.
     *
     * @param e the element referencing
     * @param uri the uri of the referenced element
     */
    public Element getReferencedElement(Element e, String uri) {
        try {
            SVGDocument document = (SVGDocument)e.getOwnerDocument();
            URIResolver ur = new URIResolver(document, documentLoader);
            Element ref = ur.getElement(uri, e);
            if (ref == null) {
                throw new BridgeException(e, ERR_URI_BAD_TARGET,
                                          new Object[] {uri});
            } else {
                return ref;
            }
        } catch (MalformedURLException ex) {
            throw new BridgeException(e, ERR_URI_MALFORMED,
                                      new Object[] {uri});
        } catch (InterruptedIOException ex) {
            throw new InterruptedBridgeException();
        } catch (IOException ex) {
            throw new BridgeException(e, ERR_URI_IO,
                                      new Object[] {uri});
        } catch (IllegalArgumentException ex) {
            throw new BridgeException(e, ERR_URI_REFERENCE_A_DOCUMENT,
                                      new Object[] {uri});
        } catch (SecurityException ex) {
            throw new BridgeException(e, ERR_URI_UNSECURE,
                                      new Object[] {uri});
        }
    }

    // Viewport //////////////////////////////////////////////////////////////

    /**
     * Returns the viewport of the specified element.
     *
     * @param e the element interested in its viewport
     */
    public Viewport getViewport(Element e) {
        if (viewportStack != null) {
            // building time
            if (viewportStack.size() == 0) {
                // outermost svg element
                return (Viewport)viewportMap.get(userAgent);
            } else {
                // current viewport
                return (Viewport)viewportStack.get(0);
            }
        } else {
            // search the first parent which has defined a viewport
            e = SVGUtilities.getParentElement(e);
            while (e != null) {
                Viewport viewport = (Viewport)viewportMap.get(e);
                if (viewport != null) {
                    return viewport;
                }
                e = SVGUtilities.getParentElement(e);
            }
            return (Viewport)viewportMap.get(userAgent);
        }
    }

    /**
     * Starts a new viewport from the specified element.
     *
     * @param e the element that defines a new viewport
     * @param viewport the viewport of the element
     */
    public void openViewport(Element e, Viewport viewport) {
        viewportMap.put(e, viewport);
        if (viewportStack == null) {
            viewportStack = new LinkedList();
        }
        viewportStack.add(0, viewport);
    }

    public void removeViewport(Element e) {
        viewportMap.remove(e);
    }

    /**
     * Closes the viewport associated to the specified element.
     * @param e the element that closes its viewport
     */
    public void closeViewport(Element e) {
        //viewportMap.remove(e); FIXME: potential memory leak
        viewportStack.remove(0);
        if (viewportStack.size() == 0) {
            viewportStack = null;
        }
    }

    // Bindings //////////////////////////////////////////////////////////////

    /**
     * Binds the specified GraphicsNode to the specified Element. This method
     * automatically bind the graphics node to the element and the element to
     * the graphics node.
     *
     * @param element the element to bind to the specified graphics node
     * @param node the graphics node to bind to the specified element
     */
    public void bind(Element element, GraphicsNode node) {
        if (elementNodeMap == null) {
            elementNodeMap = new WeakHashMap();
            nodeElementMap = new WeakHashMap();
        }
        elementNodeMap.put(element, new SoftReference(node));
        nodeElementMap.put(node, new SoftReference(element));
    }

    /**
     * Removes the binding of the specified Element.
     *
     * @param element the element to unbind
     */
    public void unbind(Element element) {
        if (elementNodeMap == null) {
            return;
        }
        GraphicsNode node = null;
        SoftReference sr = (SoftReference)elementNodeMap.get(element);
        if (sr != null)
            node = (GraphicsNode)sr.get();
        elementNodeMap.remove(element);
        if (node != null)
            nodeElementMap.remove(node);
    }

    /**
     * Returns the GraphicsNode associated to the specified Element or
     * null if any.
     *
     * @param element the element associated to the graphics node to return
     */
    public GraphicsNode getGraphicsNode(Element element) {
        if (elementNodeMap != null) {
            SoftReference sr = (SoftReference)elementNodeMap.get(element);
            if (sr != null) 
                return (GraphicsNode)sr.get();
        }
        return null;
    }

    /**
     * Returns the Element associated to the specified GraphicsNode or
     * null if any.
     *
     * @param node the graphics node associated to the element to return
     */
    public Element getElement(GraphicsNode node) {
        if (nodeElementMap != null) {
            SoftReference sr = (SoftReference)nodeElementMap.get(node);
            if (sr != null) 
                return (Element)sr.get();
        }
        return null;
    }

    // Bridge management /////////////////////////////////////////////////////
 
    /**
     * Returns true if the specified element has a GraphicsNodeBridge
     * associated to it, false otherwise.
     *
     * @param element the element
     */
    public boolean hasGraphicsNodeBridge(Element element) {
        if (namespaceURIMap == null || element == null) {
            return false;
        }
        String localName = element.getLocalName();
        String namespaceURI = element.getNamespaceURI();
        namespaceURI = ((namespaceURI == null)? "" : namespaceURI);
        HashMap localNameMap = (HashMap) namespaceURIMap.get(namespaceURI);
        if (localNameMap == null) {
            return false;
        }
        return (localNameMap.get(localName) instanceof GraphicsNodeBridge);
    }

    /**
     * Returns the bridge associated with the specified element.
     *
     * @param element the element
     */
    public Bridge getBridge(Element element) {
        if (namespaceURIMap == null || element == null) {
            return null;
        }
        String localName = element.getLocalName();
        String namespaceURI = element.getNamespaceURI();
        namespaceURI = ((namespaceURI == null)? "" : namespaceURI);
        return getBridge(namespaceURI, localName);
    }

    /**
     * Returns the bridge associated with the element type
     *
     * @param nameSpaceURI namespace of the requested element
     * @param localName element's local name
     *
     */
    public Bridge getBridge(String namespaceURI, String localName) {
        HashMap localNameMap = (HashMap) namespaceURIMap.get(namespaceURI);
        if (localNameMap == null) {
            return null;
        }
        Bridge bridge = (Bridge)localNameMap.get(localName);
        if (isDynamic()) {
            return bridge == null ? null : bridge.getInstance();
        } else {
            return bridge;
        }
    }

    /**
     * Associates the specified <tt>Bridge</tt> object with the specified
     * namespace URI and local name.
     * @param namespaceURI the namespace URI
     * @param localName the local name
     * @param bridge the bridge that manages the element
     */
    public void putBridge(String namespaceURI, String localName, Bridge bridge) {
        // start assert
        if (!(namespaceURI.equals(bridge.getNamespaceURI())
              && localName.equals(bridge.getLocalName()))) {
            throw new Error("Invalid Bridge: "+
                            namespaceURI+"/"+bridge.getNamespaceURI()+" "+
                            localName+"/"+bridge.getLocalName()+" "+
                            bridge.getClass());
        }
        // end assert
        if (namespaceURIMap == null) {
            namespaceURIMap = new HashMap();
        }
        namespaceURI = ((namespaceURI == null)? "" : namespaceURI);
        HashMap localNameMap = (HashMap) namespaceURIMap.get(namespaceURI);
        if (localNameMap == null) {
            localNameMap = new HashMap();
            namespaceURIMap.put(namespaceURI, localNameMap);
        }
        localNameMap.put(localName, bridge);
    }

    /**
     * Associates the specified <tt>Bridge</tt> object with it's 
     * namespace URI and local name.
     *
     * @param bridge the bridge that manages the element
     */
    public void putBridge(Bridge bridge) {
        putBridge(bridge.getNamespaceURI(), bridge.getLocalName(), bridge);
    }

    /**
     * Removes the <tt>Bridge</tt> object associated to the specified
     * namespace URI and local name.
     *
     * @param namespaceURI the namespace URI
     * @param localName the local name
     */
    public void removeBridge(String namespaceURI, String localName) {
        if (namespaceURIMap == null) {
            return;
        }
        namespaceURI = ((namespaceURI == null)? "" : namespaceURI);
        HashMap localNameMap = (HashMap) namespaceURIMap.get(namespaceURI);
        if (localNameMap != null) {
            localNameMap.remove(localName);
            if (localNameMap.isEmpty()) {
                namespaceURIMap.remove(namespaceURI);
                if (namespaceURIMap.isEmpty()) {
                    namespaceURIMap = null;
                }
            }
        }
    }

    // dynamic support ////////////////////////////////////////////////////////

    /**
     * The list of all EventListener attached by bridges that need to
     * be removed on a dispose() call.
     */
    protected Set eventListenerSet = new HashSet();

    /**
     * The DOM EventListener to receive 'DOMCharacterDataModified' event.
     */
    protected EventListener domCharacterDataModifiedListener;

    /**
     * The DOM EventListener to receive 'DOMAttrModified' event.
     */
    protected EventListener domAttrModifiedEventListener;

    /**
     * The DOM EventListener to receive 'DOMNodeInserted' event.
     */
    protected EventListener domNodeInsertedEventListener;

    /**
     * The DOM EventListener to receive 'DOMNodeRemoved' event.
     */
    protected EventListener domNodeRemovedEventListener;

    /**
     * The CSSEngine listener to receive CSSEngineEvent.
     */
    protected CSSEngineListener cssPropertiesChangedListener;

    /**
     * The EventListener that is responsible of managing DOM focus event.
     */
    protected FocusManager focusManager;

    /**
     * Manages cursors and performs caching when appropriate
     */
    protected CursorManager cursorManager = new CursorManager(this);

    /**
     * Adds EventListeners to the input document to handle the cursor 
     * property.
     * This is not done in the addDOMListeners method because 
     * addDOMListeners is only used for dynamic content whereas 
     * cursor support is provided for all content.
     * Also note that it is very important that the listeners be
     * registered for the capture phase as the 'default' behavior
     * for cursors is handled by the BridgeContext during the 
     * capture phase and the 'custom' behavior (handling of 'auto'
     * on anchors, for example), is handled during the bubling phase.
     */
    public void addUIEventListeners(Document doc) {
        EventTarget evtTarget = (EventTarget)doc.getDocumentElement();

        DOMMouseOverEventListener domMouseOverListener =
            new DOMMouseOverEventListener();
        evtTarget.addEventListener(SVGConstants.SVG_EVENT_MOUSEOVER, 
                                   domMouseOverListener,
                                   true);
        storeEventListener(evtTarget, SVGConstants.SVG_EVENT_MOUSEOVER, 
                           domMouseOverListener, true);

        DOMMouseOutEventListener domMouseOutListener =
            new DOMMouseOutEventListener();
        evtTarget.addEventListener(SVGConstants.SVG_EVENT_MOUSEOUT,
                                   domMouseOutListener,
                                   true);
        storeEventListener(evtTarget, SVGConstants.SVG_EVENT_MOUSEOUT, 
                           domMouseOutListener, true);

    }

    /**
     * Adds EventListeners to the DOM and CSSEngineListener to the
     * CSSEngine to handle any modifications on the DOM tree or style
     * properties and update the GVT tree in response.
     */
    public void addDOMListeners() {
        EventTarget evtTarget = (EventTarget)document;

        domAttrModifiedEventListener = new DOMAttrModifiedEventListener();
        evtTarget.addEventListener("DOMAttrModified",
                                   domAttrModifiedEventListener,
                                   true);

        domNodeInsertedEventListener = new DOMNodeInsertedEventListener();
        evtTarget.addEventListener("DOMNodeInserted",
                                   domNodeInsertedEventListener,
                                   true);

        domNodeRemovedEventListener = new DOMNodeRemovedEventListener();
        evtTarget.addEventListener("DOMNodeRemoved",
                                   domNodeRemovedEventListener,
                                   true);

        domCharacterDataModifiedListener = 
            new DOMCharacterDataModifiedListener();
        evtTarget.addEventListener("DOMCharacterDataModified",
                                   domCharacterDataModifiedListener,
                                   true);


        focusManager = new FocusManager(document);

        SVGOMDocument svgDocument = (SVGOMDocument)document;
        CSSEngine cssEngine = svgDocument.getCSSEngine();
        cssPropertiesChangedListener = new CSSPropertiesChangedListener();
        cssEngine.addCSSEngineListener(cssPropertiesChangedListener);
    }

    /**
     * Adds to the eventListenerSet the specified event listener
     * registration.
     */
    protected void storeEventListener(EventTarget t,
                                      String s,
                                      EventListener l,
                                      boolean b) {
        synchronized (eventListenerSet) {
            eventListenerSet.add(new EventListenerMememto(t, s, l, b, this));
        }
    }

    public static class SoftReferenceMememto 
        extends CleanerThread.SoftReferenceCleared {
        Object mememto;
        Set    set;
        SoftReferenceMememto(Object ref, Object mememto, Set set) {
            super(ref);
            this.mememto = mememto;
            this.set     = set;
        }

        public void cleared() {
            synchronized (set) {
                set.remove(mememto);
                mememto = null;
                set     = null;
            }
        }
    }

    /**
     * A class used to store an EventListener added to the DOM.
     */
    protected static class EventListenerMememto {

        public SoftReference target; // Soft ref to EventTarget
        public SoftReference listener; // Soft ref to EventListener 
        public boolean useCapture;
        public String eventType;

        public EventListenerMememto(EventTarget t, 
                                    String s, 
                                    EventListener l, 
                                    boolean b,
                                    BridgeContext ctx) {
            Set set = ctx.eventListenerSet;
            target = new SoftReferenceMememto(t, this, set);
            listener = new SoftReferenceMememto(l, this, set);
            eventType = s;
            useCapture = b;
        }

        public EventListener getListener() {
            return (EventListener)listener.get();
        }
        public EventTarget getTarget() {
            return (EventTarget)target.get();
        }
        public boolean getUseCapture() {
            return useCapture;
        }
        public String getEventType() {
            return eventType;
        }
    }


    /**
     * Disposes this BridgeContext.
     */
    public void dispose() {

        synchronized (eventListenerSet) {
        // remove all listeners added by Bridges
            Iterator iter = eventListenerSet.iterator();
        while (iter.hasNext()) {
            EventListenerMememto m = (EventListenerMememto)iter.next();
            EventTarget   et = m.getTarget();
            EventListener el = m.getListener();
            boolean       uc = m.getUseCapture();
            String        t  = m.getEventType();
            if ((et == null) || (el == null) || (t == null))
                continue;
            et.removeEventListener(t, el, uc);
            }
        }

        if (document != null) {
            EventTarget evtTarget = (EventTarget)document;

            evtTarget.removeEventListener("DOMAttrModified",
                                          domAttrModifiedEventListener, 
                                          true);
            evtTarget.removeEventListener("DOMNodeInserted",
                                          domNodeInsertedEventListener, 
                                          true);
            evtTarget.removeEventListener("DOMNodeRemoved",
                                          domNodeRemovedEventListener, 
                                          true);
            evtTarget.removeEventListener("DOMCharacterDataModified",
                                          domCharacterDataModifiedListener, 
                                          true);
            
            SVGOMDocument svgDocument = (SVGOMDocument)document;
            CSSEngine cssEngine = svgDocument.getCSSEngine();
            if (cssEngine != null) {
                cssEngine.removeCSSEngineListener
                    (cssPropertiesChangedListener);
                cssEngine.dispose();
                svgDocument.setCSSEngine(null);
            }
        }
        Iterator iter = interpreterMap.values().iterator();
        while (iter.hasNext()) {
            Interpreter interpreter = (Interpreter)iter.next();
            if (interpreter != null)
                interpreter.dispose();
        }
        interpreterMap.clear();

        if (focusManager != null) {
            focusManager.dispose();
        }
    }

    /**
     * Returns the SVGContext associated to the specified Node or null if any.
     */
    protected static SVGContext getSVGContext(Node node) {
        if (node instanceof SVGOMElement) {
            return ((SVGOMElement)node).getSVGContext();
        } else {
            return null;
        }
    }

    /**
     * Returns the SVGContext associated to the specified Node or null if any.
     */
    protected static BridgeUpdateHandler getBridgeUpdateHandler(Node node) {
        SVGContext ctx = getSVGContext(node);
        return (ctx == null) ? null : (BridgeUpdateHandler)ctx;
    }

    /**
     * The DOM EventListener invoked when an attribute is modified.
     */
    protected class DOMAttrModifiedEventListener implements EventListener {

        /**
         * Handles 'DOMAttrModified' event type.
         */
        public void handleEvent(Event evt) {
            Node node = (Node)evt.getTarget();
            BridgeUpdateHandler h = getBridgeUpdateHandler(node);
            if (h != null) {
                try {
                    h.handleDOMAttrModifiedEvent((MutationEvent)evt);
                } catch (Exception e) {
                    userAgent.displayError(e);
                }
            }
        }
    }

    /**
     * The DOM EventListener invoked when the mouse exits an element
     */
    protected class DOMMouseOutEventListener implements EventListener {
        public void handleEvent(Event evt) {
            MouseEvent me = (MouseEvent)evt;
            Element newTarget = (Element)me.getRelatedTarget();
            Cursor cursor = CursorManager.DEFAULT_CURSOR;
            if (newTarget != null)
                cursor = CSSUtilities.convertCursor
                    (newTarget, BridgeContext.this);
            if (cursor == null) 
                cursor = CursorManager.DEFAULT_CURSOR;

            userAgent.setSVGCursor(cursor);
        }
    }


    /**
     * The DOM EventListener invoked when the mouse mouves over a new 
     * element.
     * 
     * Here is how cursors are handled:
     * 
     */
    protected class DOMMouseOverEventListener implements EventListener {
        /**
         * Handles 'mouseover' MouseEvent event type.
         */
        public void handleEvent(Event evt) {
            Element target = (Element)evt.getTarget();
            Cursor cursor = CSSUtilities.convertCursor(target, BridgeContext.this);
            
            if (cursor != null) {
                userAgent.setSVGCursor(cursor);
            }
        }
    }
    
    /**
     * The DOM EventListener invoked when a node is added.
     */
    protected class DOMNodeInsertedEventListener implements EventListener {

        /**
         * Handles 'DOMNodeInserted' event type.
         */
        public void handleEvent(Event evt) {
            MutationEvent me = (MutationEvent)evt;
            BridgeUpdateHandler h = 
                getBridgeUpdateHandler(me.getRelatedNode());
            if (h != null) {
                try {
                    h.handleDOMNodeInsertedEvent(me);
                } catch (InterruptedBridgeException ibe) {
                    /* do nothing */
                } catch (Exception e) {
                    userAgent.displayError(e);
                }
            }
        }
    }

    /**
     * The DOM EventListener invoked when a node is removed.
     */
    protected class DOMNodeRemovedEventListener implements EventListener {

        /**
         * Handles 'DOMNodeRemoved' event type.
         */
        public void handleEvent(Event evt) {
            Node node = (Node)evt.getTarget();
            BridgeUpdateHandler h = getBridgeUpdateHandler(node);
            if (h != null) {
                try {
                    h.handleDOMNodeRemovedEvent((MutationEvent)evt);
                } catch (Exception e) {
                    userAgent.displayError(e);
                }
            }
        }
    }

    /**
     * The DOM EventListener invoked when a character data is changed.
     */
    protected class DOMCharacterDataModifiedListener implements EventListener {

        /**
         * Handles 'DOMNodeRemoved' event type.
         */
        public void handleEvent(Event evt) {
            Node node = (Node)evt.getTarget();
            while (node != null && !(node instanceof SVGOMElement)) {
                node = node.getParentNode();
            }
            BridgeUpdateHandler h = getBridgeUpdateHandler(node);
            if (h != null) {
                try {
                    h.handleDOMCharacterDataModified((MutationEvent)evt);
                } catch (Exception e) {
                    userAgent.displayError(e);
                }
            }
        }
    }

    /**
     * The CSSEngineListener invoked when CSS properties are modified
     * on a particular element.
     */
    protected class CSSPropertiesChangedListener implements CSSEngineListener {

        /**
         * Handles CSSEngineEvent that describes the CSS properties
         * that have changed on a particular element.
         */
        public void propertiesChanged(CSSEngineEvent evt) {
            Element elem = evt.getElement();
            SVGContext ctx = getSVGContext(elem);
            if (ctx == null) {
                GraphicsNode pgn = getGraphicsNode
                    ((Element)elem.getParentNode());
                if ((pgn == null) || !(pgn instanceof CompositeGraphicsNode)) {
                    // Something changed in this element but we really don't
                    // care since it's parent isn't displayed either.
                    return;
                }
                CompositeGraphicsNode parent = (CompositeGraphicsNode)pgn;
                // Check if 'display' changed on this element.

                int [] properties = evt.getProperties();
                for (int i=0; i < properties.length; ++i) {
                    if (properties[i] == SVGCSSEngine.DISPLAY_INDEX) {
                        if (!CSSUtilities.convertDisplay(elem)) {
                            // (Still) Not displayed
                            break;
                        }
                        // build the graphics node
                        GVTBuilder builder = getGVTBuilder();
                        GraphicsNode childNode = builder.build
                            (BridgeContext.this, elem);
                        if (childNode == null) {
                            // the added element is not a graphic element?
                            break; 
                        }
                        int idx = -1;
                        for(Node ps = elem.getPreviousSibling(); ps != null;
                            ps = ps.getPreviousSibling()) {
                            if (ps.getNodeType() != Node.ELEMENT_NODE)
                                continue;
                            Element pse = (Element)ps;
                            GraphicsNode gn = getGraphicsNode(pse);
                            if (gn == null)
                                continue;
                            idx = parent.indexOf(gn);
                            if (idx == -1)
                                continue;
                            break;
                        }
                        // insert after prevSibling, if
                        // it was -1 this becomes 0 (first slot)
                        idx++; 
                        parent.add(idx, childNode);
                        break;
                    }
                }
            } if (ctx != null && (ctx instanceof BridgeUpdateHandler)) {
                ((BridgeUpdateHandler)ctx).handleCSSEngineEvent(evt);
            }
        }
    }

    // CSS context ////////////////////////////////////////////////////////////

    /**
     * Returns the Value corresponding to the given system color.
     */
    public Value getSystemColor(String ident) {
        return SystemColorSupport.getSystemColor(ident);
    }

    /**
     * Returns the value corresponding to the default font.
     */
    public Value getDefaultFontFamily() {
        // No cache needed since the default font family is asked only
        // one time on the root element (only if it does not have its
        // own font-family).
        SVGOMDocument      doc  = (SVGOMDocument)document;
        SVGStylableElement root = (SVGStylableElement)doc.getRootElement();
        String str = userAgent.getDefaultFontFamily();
        return doc.getCSSEngine().parsePropertyValue
            (root,SVGConstants.CSS_FONT_FAMILY_PROPERTY, str);
    }

    /**
     * Returns a lighter font-weight.
     */
    public float getLighterFontWeight(float f) {
        return userAgent.getLighterFontWeight(f);
    }

    /**
     * Returns a bolder font-weight.
     */
    public float getBolderFontWeight(float f) {
        return userAgent.getBolderFontWeight(f);
    }

    /**
     * Returns the size of a px CSS unit in millimeters.
     */
    public float getPixelUnitToMillimeter() {
        return userAgent.getPixelUnitToMillimeter();
    }

    /**
     * Returns the size of a px CSS unit in millimeters.
     * This will be removed after next release.
     * @see #getPixelUnitToMillimeter()
     */
    public float getPixelToMillimeter() {
        return getPixelUnitToMillimeter();
            
    }

    /**
     * Returns the medium font size.
     */
    public float getMediumFontSize() {
        return userAgent.getMediumFontSize();
    }

    /**
     * Returns the width of the block which directly contains the
     * given element.
     */
    public float getBlockWidth(Element elt) {
        return getViewport(elt).getWidth();
    }

    /**
     * Returns the height of the block which directly contains the
     * given element.
     */
    public float getBlockHeight(Element elt) {
        return getViewport(elt).getHeight();
    }

    /**
     * This method throws a SecurityException if the resource
     * found at url and referenced from docURL
     * should not be loaded.
     * 
     * This is a convenience method to call checkLoadExternalResource
     * on the ExternalResourceSecurity strategy returned by 
     * getExternalResourceSecurity.
     *
     * @param scriptURL url for the script, as defined in
     *        the script's xlink:href attribute. If that
     *        attribute was empty, then this parameter should
     *        be null
     * @param docURL url for the document into which the 
     *        script was found.
     */
    public void 
        checkLoadExternalResource(ParsedURL resourceURL,
                                  ParsedURL docURL) throws SecurityException {
        userAgent.checkLoadExternalResource(resourceURL,
                                            docURL);
    }


    /**
     * Tells whether the given SVG document is dynamic.
     */
    public static boolean isDynamicDocument(Document doc) {
        return BaseScriptingEnvironment.isDynamicDocument(doc);
    }
    
    /**
     * Tells whether the given SVG document is Interactive.
     * We say it is, if it has any <title>, <desc>, or <a> elements,
     * of if the 'cursor' property is anything but Auto on any element.
     */
    public static boolean isInteractiveDocument(Document doc) {
        
        Element root = ((SVGDocument)doc).getRootElement();
        if (!SVGConstants.SVG_NAMESPACE_URI.equals(root.getNamespaceURI()))
            return false;

        return checkInteractiveElement(root);
    }

    public static boolean checkInteractiveElement(Element e) {
        String tag = e.getNodeName();

        // Check if it's one of our important element.
        if (SVGConstants.SVG_A_TAG.equals(tag))
            return true;
        if (SVGConstants.SVG_TITLE_TAG.equals(tag))
            return true;
        if (SVGConstants.SVG_DESC_TAG.equals(tag))
            return true;
        if (SVGConstants.SVG_CURSOR_TAG.equals(tag))
            return true;

        // I am well aware that this is not 100% accurate but it's
        // the best I can do w/o booting the CSSEngine.
        if (e.getAttribute(CSSConstants.CSS_CURSOR_PROPERTY).length() >0)
            return true;

        /* We would like to do this but the CSS Engine isn't setup when
           we want to do this.
           
        // Check if cursor property is set to something other than 'auto'.
        Value cursorValue = CSSUtilities.getComputedStyle
            (e, SVGCSSEngine.CURSOR_INDEX);
        if ((cursorValue != null) &&
            (cursorValue.getCssValueType()  == CSSValue.CSS_PRIMITIVE_VALUE) &&
            (cursorValue.getPrimitiveType() == CSSPrimitiveValue.CSS_IDENT) &&
            (SVGConstants.SVG_AUTO_VALUE.equals(cursorValue.getStringValue())))
            return true;
        */

        // Check all the child elements for any of the above.
        final String svg_ns = SVGConstants.SVG_NAMESPACE_URI;
        for (Node n = e.getFirstChild();
             n != null;
             n = n.getNextSibling()) {
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                Element child = (Element)n;
                if (svg_ns.equals(child.getNamespaceURI()))
                    if (checkInteractiveElement(child))
                        return true;
            }
        }
        return false;
    }
    

    // bridge extensions support //////////////////////////////////////////////

    protected static List extensions = null;

    /**
     * Registers the bridges to handle SVG 1.0 elements.
     *
     * @param ctx the bridge context to initialize
     */
    public static void registerSVGBridges(BridgeContext ctx) {
        UserAgent ua = ctx.getUserAgent();
        Iterator iter = getBridgeExtensions().iterator();
        while(iter.hasNext()) {
            BridgeExtension be = (BridgeExtension)iter.next();
            be.registerTags(ctx);
            ua.registerExtension(be);
        }
    }

    /**
     * Returns the extensions supported by this bridge context.
     */
    public synchronized static List getBridgeExtensions() {
        if (extensions != null) {
            return extensions;
        }
        extensions = new LinkedList();
        extensions.add(new SVGBridgeExtension());

        Iterator iter = Service.providers(BridgeExtension.class);

        while (iter.hasNext()) {
            BridgeExtension be = (BridgeExtension)iter.next();
            float priority  = be.getPriority();
            ListIterator li = extensions.listIterator();
            for (;;) {
                if (!li.hasNext()) {
                    li.add(be);
                    break;
                }
                BridgeExtension lbe = (BridgeExtension)li.next();
                if (lbe.getPriority() > priority) {
                    li.previous();
                    li.add(be);
                    break;
                }
            }
        }
        return extensions;
    }        

    public static class CSSEngineUserAgentWrapper implements CSSEngineUserAgent {
        UserAgent ua;
        CSSEngineUserAgentWrapper(UserAgent ua) {
            this.ua = ua;
        }

        /**
         * Displays an error resulting from the specified Exception.
         */
        public void displayError(Exception ex) { ua.displayError(ex); }

        /**
         * Displays a message in the User Agent interface.
         */
        public void displayMessage(String message) { ua.displayMessage(message); }
    }

}

