/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

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 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.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.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.css.CSSPrimitiveValue;
import org.w3c.dom.css.CSSValue;
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.68 2003/07/10 12:01:18 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 HashMap();

    /**
     * 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 HashMap elementNodeMap;

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

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

    /**
     * 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);
            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;
    }

    /**
     * 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 HashMap();
            nodeElementMap = new HashMap();
        }
        elementNodeMap.put(element, node);
        nodeElementMap.put(node, 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 = (GraphicsNode)elementNodeMap.get(element);
        elementNodeMap.remove(element);
        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) {
            return (GraphicsNode)elementNodeMap.get(element);
        } else {
            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) {
            return (Element)nodeElementMap.get(node);
        } else {
            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();
            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) {
            SVGContext ctx = getSVGContext(evt.getElement());
            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;
    }        
 }

