/*

 ============================================================================
                   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.css.engine;

import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.batik.css.engine.sac.CSSConditionFactory;
import org.apache.batik.css.engine.sac.CSSSelectorFactory;
import org.apache.batik.css.engine.sac.ExtendedSelector;
import org.apache.batik.css.engine.value.ComputedValue;
import org.apache.batik.css.engine.value.InheritValue;
import org.apache.batik.css.engine.value.ShorthandManager;
import org.apache.batik.css.engine.value.Value;
import org.apache.batik.css.engine.value.ValueManager;
import org.apache.batik.css.parser.ExtendedParser;
import org.apache.batik.util.CSSConstants;
import org.apache.batik.util.ParsedURL;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.DocumentHandler;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.LexicalUnit;
import org.w3c.css.sac.SACMediaList;
import org.w3c.css.sac.SelectorList;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
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.MutationEvent;

/**
 * This is the base class for all the CSS engines.
 *
 * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
 * @version $Id: CSSEngine.java,v 1.37 2003/12/14 18:01:11 deweese Exp $
 */
public abstract class CSSEngine {

    /**
     * List of StyleMap objects, one for each @font-face rule
     * encountered by this CSSEngine.
     */
    protected List fontFaces = new LinkedList();

    /**
     * Get's the StyleMaps generated by @font-face rules
     * encountered by this CSSEngine thus far.
     */
    public List getFontFaces() { return fontFaces; }

    CSSEngineUserAgent userAgent = null;

    /**
     * Returns the next stylable parent of the given element.
     */
    public static CSSStylableElement getParentCSSStylableElement(Element elt) {
        Element e = getParentElement(elt);
        while (e != null) {
            if (e instanceof CSSStylableElement) {
                return (CSSStylableElement)e;
            }
            e = getParentElement(e);
        }
        return null;
    }

    /**
     * Returns the next parent element of the given element, from the
     * CSS point of view.
     */
    public static Element getParentElement(Element elt) {
        Node n = elt.getParentNode();
        while (n != null) {
            n = getLogicalParentNode(n);
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                return (Element)n;
            }
            n = n.getParentNode();
        }
        return null;
    }

    /**
     * Returns the logical parent of a node, given its physical parent.
     */
    public static Node getLogicalParentNode(Node parent) {
        Node node = parent;
        if (node != null) {
            if (node instanceof CSSImportedElementRoot) {
                return ((CSSImportedElementRoot)node).getCSSParentElement();
            } else {
                return node;
            }
        }
        return null;
    }

    /**
     * Returns the imported child of the given node, if any.
     */
    public static CSSImportedElementRoot getImportedChild(Node node) {
        if (node instanceof CSSImportNode) {
            CSSImportNode inode = (CSSImportNode)node;
            CSSImportedElementRoot r = inode.getCSSImportedElementRoot();
            return r;
        }
        return null;
    }

    /**
     * The CSS context.
     */
    protected CSSContext cssContext;
    
    /**
     * The associated document.
     */
    protected Document document;

    /**
     * The document URI.
     */
    protected URL documentURI;

    /**
     * The property/int mappings.
     */
    protected StringIntMap indexes;

    /**
     * The shorthand-property/int mappings.
     */
    protected StringIntMap shorthandIndexes;

    /**
     * The value managers.
     */
    protected ValueManager[] valueManagers;

    /**
     * The shorthand managers.
     */
    protected ShorthandManager[] shorthandManagers;

    /**
     * The CSS parser.
     */
    protected ExtendedParser parser;

    /**
     * The pseudo-element names.
     */
    protected String[] pseudoElementNames;

    /**
     * The font-size property index.
     */
    protected int fontSizeIndex = -1;

    /**
     * The line-height property index.
     */
    protected int lineHeightIndex = -1;

    /**
     * The color property index.
     */
    protected int colorIndex = -1;

    /**
     * The user-agent style-sheet.
     */
    protected StyleSheet userAgentStyleSheet;

    /**
     * The user style-sheet.
     */
    protected StyleSheet userStyleSheet;

    /**
     * The media to use to cascade properties.
     */
    protected SACMediaList media;

    /**
     * The DOM nodes which contains StyleSheets.
     */
    protected List styleSheetNodes;

    /**
     * The style attribute namespace URI.
     */
    protected String styleNamespaceURI;

    /**
     * The style attribute local name.
     */
    protected String styleLocalName;
    
    /**
     * The class attribute namespace URI.
     */
    protected String classNamespaceURI;

    /**
     * The class attribute local name.
     */
    protected String classLocalName;
    
    /**
     * The non CSS presentational hints.
     */
    protected Set nonCSSPresentationalHints;

    /**
     * The non CSS presentational hints namespace URI.
     */
    protected String nonCSSPresentationalHintsNamespaceURI;

    /**
     * The style declaration document handler.
     */
    protected StyleDeclarationDocumentHandler styleDeclarationDocumentHandler =
        new StyleDeclarationDocumentHandler();

    /**
     * The style declaration update handler.
     */
    protected StyleDeclarationUpdateHandler styleDeclarationUpdateHandler;

    /**
     * The style sheet document handler.
     */
    protected StyleSheetDocumentHandler styleSheetDocumentHandler =
        new StyleSheetDocumentHandler();

    /**
     * The style declaration document handler used to build a
     * StyleDeclaration object.
     */
    protected StyleDeclarationBuilder styleDeclarationBuilder =
        new StyleDeclarationBuilder();

    /**
     * The current element.
     */
    protected CSSStylableElement element;

    /**
     * The current base URI.
     */
    protected URL cssBaseURI;

    /**
     * The alternate stylesheet title.
     */
    protected String alternateStyleSheet;

    /**
     * The DOMAttrModified event listener.
     */
    protected EventListener domAttrModifiedListener;

    /**
     * The DOMNodeInserted event listener.
     */
    protected EventListener domNodeInsertedListener;

    /**
     * The DOMNodeRemoved event listener.
     */
    protected EventListener domNodeRemovedListener;

    /**
     * The DOMSubtreeModified event listener.
     */
    protected EventListener domSubtreeModifiedListener;

    /**
     * The DOMCharacterDataModified event listener.
     */
    protected EventListener domCharacterDataModifiedListener;

    /**
     * Whether a style sheet as been removed from the document.
     */
    protected boolean styleSheetRemoved;

    /**
     * The right sibling of the last removed node.
     */
    protected Node removedStylableElementSibling;

    /**
     * The listeners.
     */
    protected List listeners = Collections.synchronizedList(new LinkedList());

    /**
     * The attributes found in stylesheets selectors.
     */
    protected Set selectorAttributes;

    /**
     * Used to fire a change event for all the properties.
     */
    protected final int[] ALL_PROPERTIES;

    /**
     * The CSS condition factory.
     */
    protected CSSConditionFactory cssConditionFactory;

    /**
     * Creates a new CSSEngine.
     * @param doc The associated document.
     * @param uri The document URI.
     * @param p The CSS parser.
     * @param vm The property value managers.
     * @param sm The shorthand properties managers.
     * @param pe The pseudo-element names supported by the associated
     *           XML dialect. Must be null if no support for pseudo-
     *           elements is required.
     * @param sns The namespace URI of the style attribute.
     * @param sln The local name of the style attribute.
     * @param cns The namespace URI of the class attribute.
     * @param cln The local name of the class attribute.
     * @param hints Whether the CSS engine should support non CSS
     *              presentational hints.
     * @param hintsNS The hints namespace URI.
     * @param ctx The CSS context.
     */
    protected CSSEngine(Document doc,
                        URL uri,
                        ExtendedParser p,
                        ValueManager[] vm,
                        ShorthandManager[] sm,
                        String[] pe,
                        String sns,
                        String sln,
                        String cns,
                        String cln,
                        boolean hints,
                        String hintsNS,
                        CSSContext ctx) {
        document = doc;
        documentURI = uri;
        parser = p;
        pseudoElementNames = pe;
        styleNamespaceURI = sns;
        styleLocalName = sln;
        classNamespaceURI = cns;
        classLocalName = cln;
        cssContext = ctx;

        cssConditionFactory = new CSSConditionFactory(cns, cln, null, "id");

        int len = vm.length;
        indexes = new StringIntMap(len);
        valueManagers = vm;

        for (int i = len - 1; i >= 0; --i) {
            String pn = vm[i].getPropertyName();
            indexes.put(pn, i);
            if (fontSizeIndex == -1 &&
                pn.equals(CSSConstants.CSS_FONT_SIZE_PROPERTY)) {
                fontSizeIndex = i;
            }
            if (lineHeightIndex == -1 &&
                pn.equals(CSSConstants.CSS_LINE_HEIGHT_PROPERTY)) {
                lineHeightIndex = i;
            }
            if (colorIndex == -1 &&
                pn.equals(CSSConstants.CSS_COLOR_PROPERTY)) {
                colorIndex = i;
            }
        }

        len = sm.length;
        shorthandIndexes = new StringIntMap(len);
        shorthandManagers = sm;
        for (int i = len - 1; i >= 0; --i) {
            shorthandIndexes.put(sm[i].getPropertyName(), i);
        }

        if (hints) {
            len = vm.length;
            nonCSSPresentationalHints = new HashSet();
            nonCSSPresentationalHintsNamespaceURI = hintsNS;
            for (int i = len - 1; i >= 0; --i) {
                String pn = vm[i].getPropertyName();
                nonCSSPresentationalHints.add(pn);
            }
        }

        if (cssContext.isDynamic() &&
            (document instanceof EventTarget)) {
            // Attach the mutation events listeners.
            EventTarget et = (EventTarget)document;
            domAttrModifiedListener = new DOMAttrModifiedListener();
            et.addEventListener("DOMAttrModified",
                                domAttrModifiedListener,
                                false);
            domNodeInsertedListener = new DOMNodeInsertedListener();
            et.addEventListener("DOMNodeInserted",
                                domNodeInsertedListener,
                                false);
            domNodeRemovedListener = new DOMNodeRemovedListener();
            et.addEventListener("DOMNodeRemoved",
                                domNodeRemovedListener,
                                false);
            domSubtreeModifiedListener = new DOMSubtreeModifiedListener();
            et.addEventListener("DOMSubtreeModified",
                                domSubtreeModifiedListener,
                                false);
            domCharacterDataModifiedListener =
                new DOMCharacterDataModifiedListener();
            et.addEventListener("DOMCharacterDataModified",
                                domCharacterDataModifiedListener,
                                false);
            styleDeclarationUpdateHandler =
                new StyleDeclarationUpdateHandler();
        }

        ALL_PROPERTIES = new int[getNumberOfProperties()];
        for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
            ALL_PROPERTIES[i] = i;
        }
    }

    /**
     * Disposes the CSSEngine and all the attached resources.
     */
    public void dispose() {
        setCSSEngineUserAgent(null);
        disposeStyleMaps(document.getDocumentElement());
        if (document instanceof EventTarget) {
            // Detach the mutation events listeners.
            EventTarget et = (EventTarget)document;
            et.removeEventListener("DOMAttrModified",
                                   domAttrModifiedListener,
                                   false);
            et.removeEventListener("DOMNodeInserted",
                                   domNodeInsertedListener,
                                   false);
            et.removeEventListener("DOMNodeRemoved",
                                   domNodeRemovedListener,
                                   false);
            et.removeEventListener("DOMSubtreeModified",
                                   domSubtreeModifiedListener,
                                   false);
            et.removeEventListener("DOMCharacterDataModified",
                                   domCharacterDataModifiedListener,
                                   false);
        }
    }

    private void disposeStyleMaps(Node node) {
        if (node instanceof CSSStylableElement) {
            ((CSSStylableElement)node).setComputedStyleMap(null, null);
        }
        for (Node n = node.getFirstChild();
             n != null;
             n = n.getNextSibling()) {
            if (n.getNodeType() == Node.ELEMENT_NODE) {
                disposeStyleMaps(n);
            }
            Node c = getImportedChild(n);
            if (c != null) {
                disposeStyleMaps(c);
            }
        }
    }

    /**
     * Returns the CSS context.
     */
    public CSSContext getCSSContext() {
        return cssContext;
    }

    /**
     * Returns the document associated with this engine.
     */
    public Document getDocument() {
        return document;
    }

    /**
     * Returns the font-size property index.
     */
    public int getFontSizeIndex() {
        return fontSizeIndex;
    }

    /**
     * Returns the line-height property index.
     */
    public int getLineHeightIndex() {
        return lineHeightIndex;
    }

    /**
     * Returns the color property index.
     */
    public int getColorIndex() {
        return colorIndex;
    }

    /**
     * Returns the number of properties.
     */
    public int getNumberOfProperties() {
        return valueManagers.length;
    }

    /**
     * Returns the property index, or -1.
     */
    public int getPropertyIndex(String name) {
        return indexes.get(name);
    }

    /**
     * Returns the shorthand property index, or -1.
     */
    public int getShorthandIndex(String name) {
        return shorthandIndexes.get(name);
    }

    /**
     * Returns the name of the property at the given index.
     */
    public String getPropertyName(int idx) {
        return valueManagers[idx].getPropertyName();
    }

    public void setCSSEngineUserAgent(CSSEngineUserAgent userAgent) {
        this.userAgent = userAgent;
    }

    public CSSEngineUserAgent getCSSEngineUserAgent() {
        return userAgent;
    }

    /**
     * Sets the user agent style-sheet.
     */
    public void setUserAgentStyleSheet(StyleSheet ss) {
        userAgentStyleSheet = ss;
    }

    /**
     * Sets the user style-sheet.
     */
    public void setUserStyleSheet(StyleSheet ss) {
        userStyleSheet = ss;
    }

    /**
     * Returns the ValueManagers.
     */
    public ValueManager[] getValueManagers() {
        return valueManagers;
    }

    /**
     * Sets the media to use to compute the styles.
     */
    public void setMedia(String str) {
        try {
            media = parser.parseMedia(str);
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String s =Messages.formatMessage
                ("media.error", new Object[] { str, m });
            throw new DOMException(DOMException.SYNTAX_ERR, s);
        }
    }

    /**
     * Sets the alternate style-sheet title.
     */
    public void setAlternateStyleSheet(String str) {
        alternateStyleSheet = str;
    }

    /**
     * Recursively imports the cascaded style from a source element
     * to an element of the current document.
     */
    public void importCascadedStyleMaps(Element src,
                                        CSSEngine srceng,
                                        Element dest) {
        if (src instanceof CSSStylableElement) {
            CSSStylableElement csrc  = (CSSStylableElement)src;
            CSSStylableElement cdest = (CSSStylableElement)dest;

            StyleMap sm = srceng.getCascadedStyleMap(csrc, null);
            sm.setFixedCascadedStyle(true);
            cdest.setComputedStyleMap(null, sm);

            if (pseudoElementNames != null) {
                int len = pseudoElementNames.length;
                for (int i = 0; i < len; i++) {
                    String pe = pseudoElementNames[i];
                    sm = srceng.getCascadedStyleMap(csrc, pe);
                    cdest.setComputedStyleMap(pe, sm);
                }
            }
        }

        for (Node dn = dest.getFirstChild(), sn = src.getFirstChild();
             dn != null;
             dn = dn.getNextSibling(), sn = sn.getNextSibling()) {
            if (sn.getNodeType() == Node.ELEMENT_NODE) {
                importCascadedStyleMaps((Element)sn, srceng, (Element)dn);
            }
        }
    }

    /**
     * Returns the current base-url.
     */
    public URL getCSSBaseURI() {
        if (cssBaseURI == null) {
            cssBaseURI = element.getCSSBase();
        }
        return cssBaseURI;
    }

    /**
     * Returns the cascaded style of the given element/pseudo-element.
     * @param elt The stylable element.
     * @param pseudo Optional pseudo-element string (null if none).
     */
    public StyleMap getCascadedStyleMap(CSSStylableElement elt,
                                        String pseudo) {
        int props = getNumberOfProperties();
        StyleMap result = new StyleMap(props);

        // Apply the user-agent style-sheet to the result.
        if (userAgentStyleSheet != null) {
            List rules = new ArrayList();
            addMatchingRules(rules, userAgentStyleSheet, elt, pseudo);
            addRules(elt, pseudo, result, rules, StyleMap.USER_AGENT_ORIGIN);
        }

        // Apply the user properties style-sheet to the result.
        if (userStyleSheet != null) {
            List rules = new ArrayList();
            addMatchingRules(rules, userStyleSheet, elt, pseudo);
            addRules(elt, pseudo, result, rules, StyleMap.USER_ORIGIN);
        }

        element = elt;
        try {
        // Apply the non-CSS presentational hints to the result.
            if (nonCSSPresentationalHints != null) {
                NamedNodeMap attrs = elt.getAttributes();
                int len = attrs.getLength();
                for (int i = 0; i < len; i++) {
                    Node attr = attrs.item(i);
                    String an = attr.getNodeName();
                    if (nonCSSPresentationalHints.contains(an)) {
                        try {
                            LexicalUnit lu;
                            int idx = getPropertyIndex(an);
                            lu = parser.parsePropertyValue
                                (attr.getNodeValue());
                            ValueManager vm = valueManagers[idx];
                            Value v = vm.createValue(lu, this);
                            putAuthorProperty(result, idx, v, false,
                                              StyleMap.NON_CSS_ORIGIN);
                        } catch (Exception e) {
                            String m = e.getMessage();
                            if (m == null) m = "";
                            String u = ((documentURI == null)?"<unknown>":
                                        documentURI.toString());
                            String s = Messages.formatMessage
                                ("property.syntax.error.at",
                                 new Object[] { u, an, attr.getNodeValue(),m});
                            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
                            if (userAgent == null) throw de;
                            userAgent.displayError(de);
                        }
                    }
                }
            }

            // Apply the document style-sheets to the result.
            List snodes = getStyleSheetNodes();
            int slen = snodes.size();
            if (slen > 0) {
                List rules = new ArrayList();
                for (int i = 0; i < slen; i++) {
                    CSSStyleSheetNode ssn = (CSSStyleSheetNode)snodes.get(i);
                    StyleSheet ss = ssn.getCSSStyleSheet();
                    if (ss != null &&
                        (!ss.isAlternate() ||
                         ss.getTitle() == null ||
                         ss.getTitle().equals(alternateStyleSheet)) &&
                        mediaMatch(ss.getMedia())) {
                        addMatchingRules(rules, ss, elt, pseudo);
                    }
                }
                addRules(elt, pseudo, result, rules, StyleMap.AUTHOR_ORIGIN);
            }

            // Apply the inline style to the result.
            if (styleLocalName != null) {
                String style = elt.getAttributeNS(styleNamespaceURI,
                                                  styleLocalName);
                if (style.length() > 0) {
                    try {
                        parser.setSelectorFactory(CSSSelectorFactory.INSTANCE);
                        parser.setConditionFactory(cssConditionFactory);
                        styleDeclarationDocumentHandler.styleMap = result;
                        parser.setDocumentHandler
                            (styleDeclarationDocumentHandler);
                        parser.parseStyleDeclaration(style);
                        styleDeclarationDocumentHandler.styleMap = null;
                    } catch (Exception e) {
                        String m = e.getMessage();
                        if (m == null) m = "";
                        String u = ((documentURI == null)?"<unknown>":
                                    documentURI.toString());
                        String s = Messages.formatMessage
                            ("style.syntax.error.at",
                             new Object[] { u, styleLocalName, style, m});
                        DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
                        if (userAgent == null) throw de;
                        userAgent.displayError(de);
                    }
                }
            }
        } finally {
            element = null;
            cssBaseURI = null;
        }

        return result;
    }

    /**
     * Returns the computed style of the given element/pseudo for the
     * property corresponding to the given index.
     */
    public Value getComputedStyle(CSSStylableElement elt,
                                  String pseudo,
                                  int propidx) {
        StyleMap sm = elt.getComputedStyleMap(pseudo);
        if (sm == null) {
            sm = getCascadedStyleMap(elt, pseudo);
            elt.setComputedStyleMap(pseudo, sm);
        }

        Value value = sm.getValue(propidx);
        if (!sm.isComputed(propidx)) {
            Value result = value;
            ValueManager vm = valueManagers[propidx];
            CSSStylableElement p = getParentCSSStylableElement(elt);
            if (value == null && (!vm.isInheritedProperty() || p == null)) {
                result = vm.getDefaultValue();
            } else if (value != null &&
                       (value == InheritValue.INSTANCE) &&
                       p != null) {
                result = null;
            }
            if (result == null) {
                // Value is 'inherit' and p != null.
                // The pseudo class is not propagated.
                result = getComputedStyle(p, null, propidx);
                sm.putParentRelative(propidx, true);
            } else {
                // Maybe is it a relative value.
                result = vm.computeValue(elt, pseudo, this, propidx,
                                         sm, result);
            }
            if (value == null) {
                sm.putValue(propidx, result);
                sm.putNullCascaded(propidx, true);
            } else if (result != value) {
                ComputedValue cv = new ComputedValue(value);
                cv.setComputedValue(result);
                sm.putValue(propidx, cv);
                result = cv;
            }
            sm.putComputed(propidx, true);
            value = result;
        }
        return value;
    }

    /**
     * Returns the document CSSStyleSheetNodes in a list. This list is
     * updated as the document is modified.
     */
    public List getStyleSheetNodes() {
        if (styleSheetNodes == null) {
            styleSheetNodes = new ArrayList();
            selectorAttributes = new HashSet();
            // Find all the style-sheets in the document.
            findStyleSheetNodes(document);
            int len = styleSheetNodes.size();
            for (int i = 0; i < len; i++) {
                CSSStyleSheetNode ssn;
                ssn = (CSSStyleSheetNode)styleSheetNodes.get(i);
                StyleSheet ss = ssn.getCSSStyleSheet();
                if (ss != null) {
                    findSelectorAttributes(selectorAttributes, ss);
                }
            }
        }
        return styleSheetNodes;
    }

    /**
     * An auxiliary method for getStyleSheets().
     */
    protected void findStyleSheetNodes(Node n) {
        if (n instanceof CSSStyleSheetNode) {
            styleSheetNodes.add(n);
        }
        for (Node nd = n.getFirstChild();
             nd != null;
             nd = nd.getNextSibling()) {
            findStyleSheetNodes(nd);
        }
    }

    /**
     * Finds the selector attributes in the given stylesheet.
     */
    protected void findSelectorAttributes(Set attrs, StyleSheet ss) {
        int len = ss.getSize();
        for (int i = 0; i < len; i++) {
            Rule r = ss.getRule(i);
            switch (r.getType()) {
            case StyleRule.TYPE:
                StyleRule style = (StyleRule)r;
                SelectorList sl = style.getSelectorList();
                int slen = sl.getLength();
                for (int j = 0; j < slen; j++) {
                    ExtendedSelector s = (ExtendedSelector)sl.item(j);
                    s.fillAttributeSet(attrs);
                }
                break;

            case MediaRule.TYPE:
            case ImportRule.TYPE:
                MediaRule mr = (MediaRule)r;
                if (mediaMatch(mr.getMediaList())) {
                    findSelectorAttributes(attrs, mr);
                }
                break;
            }
        }
    }

    /**
     * Interface for people interesting in having 'primary' properties
     * set.  Shorthand properties will be expanded "automatically".
     */
    public interface MainPropertyReceiver {
        /**
         * Called with a non-shorthand property name and it's value.
         */
        public void setMainProperty(String name, Value v, boolean important);
    };

    public void setMainProperties
        (CSSStylableElement elt, final MainPropertyReceiver dst,
         String pname, String value, boolean important){
        try {
            element = elt;
            LexicalUnit lu = parser.parsePropertyValue(value);
            ShorthandManager.PropertyHandler ph =
                new ShorthandManager.PropertyHandler() {
                    public void property(String pname, LexicalUnit lu,
                                         boolean important) {
                        int idx = getPropertyIndex(pname);
                        if (idx != -1) {
                            ValueManager vm = valueManagers[idx];
                            Value v = vm.createValue(lu, CSSEngine.this);
                            dst.setMainProperty(pname, v, important);
                            return;
                        }
                        idx = getShorthandIndex(pname);
                        if (idx == -1)
                            return; // Unknown property...
                        // Shorthand value
                        shorthandManagers[idx].setValues
                            (CSSEngine.this, this, lu, important);
                    }
                };
            ph.property(pname, lu, important);
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String u = ((documentURI == null)?"<unknown>":
                        documentURI.toString());
            String s = Messages.formatMessage
                ("property.syntax.error.at",
                 new Object[] { u, pname, value, m});
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
        } finally {
            element = null;
            cssBaseURI = null;
        }
    }

    /**
     * Parses and creates a property value from elt.
     * @param elt  The element property is from.
     * @param prop The property name.
     * @param value The property value.
     */
    public Value parsePropertyValue(CSSStylableElement elt,
                                    String prop, String value) {
        int idx = getPropertyIndex(prop);
        if (idx == -1) return null;
        ValueManager vm = valueManagers[idx];
        try {
            element = elt;
            LexicalUnit lu;
            lu = parser.parsePropertyValue(value);
            return vm.createValue(lu, this);
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String u = ((documentURI == null)?"<unknown>":
                        documentURI.toString());
            String s = Messages.formatMessage
                ("property.syntax.error.at",
                 new Object[] { u, prop, value, m});
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
        } finally {
            element = null;
            cssBaseURI = null;
        }
        return vm.getDefaultValue();
    }

    /**
     * Parses and creates a style declaration.
     * @param value The style declaration text.
     */
    public StyleDeclaration parseStyleDeclaration(CSSStylableElement elt,
                                                  String value) {
        styleDeclarationBuilder.styleDeclaration = new StyleDeclaration();
        try {
            element = elt;
            parser.setSelectorFactory(CSSSelectorFactory.INSTANCE);
            parser.setConditionFactory(cssConditionFactory);
            parser.setDocumentHandler(styleDeclarationBuilder);
            parser.parseStyleDeclaration(value);
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String u = ((documentURI == null)?"<unknown>":
                        documentURI.toString());
            String s = Messages.formatMessage
                ("syntax.error.at", new Object[] { u, m });
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
        } finally {
            element = null;
            cssBaseURI = null;
        }
        return styleDeclarationBuilder.styleDeclaration;
    }

    /**
     * Parses and creates a new style-sheet.
     * @param uri The style-sheet URI.
     * @param media The target media of the style-sheet.
     */
    public StyleSheet parseStyleSheet(URL uri, String media)
        throws DOMException {
        StyleSheet ss = new StyleSheet();
        try {
            ss.setMedia(parser.parseMedia(media));
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String u = ((documentURI == null)?"<unknown>":
                        documentURI.toString());
            String s = Messages.formatMessage
                ("syntax.error.at", new Object[] { u, m });
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
            return ss;
        }
        parseStyleSheet(ss, uri);
        return ss;
    }

    /**
     * Parses and creates a new style-sheet.
     * @param is The input source used to read the document.
     * @param uri The base URI.
     * @param media The target media of the style-sheet.
     */
    public StyleSheet parseStyleSheet(InputSource is, URL uri, String media)
        throws DOMException {
        StyleSheet ss = new StyleSheet();
        try {
            ss.setMedia(parser.parseMedia(media));
            parseStyleSheet(ss, is, uri);
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String u = ((documentURI == null)?"<unknown>":
                        documentURI.toString());
            String s = Messages.formatMessage
                ("syntax.error.at", new Object[] { u, m });
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
        }
        return ss;
    }

    /**
     * Parses and fills the given style-sheet.
     * @param ss The stylesheet to fill.
     * @param uri The base URI.
     */
    public void parseStyleSheet(StyleSheet ss, URL uri) throws DOMException {
        if (uri == null) {
            String s = Messages.formatMessage
                ("syntax.error.at",
                 new Object[] { "Null Document reference", "" });
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
            return;
        }

	try {
            // Check that access to the uri is allowed
             ParsedURL pDocURL = null;
             if (documentURI != null) {
                 pDocURL = new ParsedURL(documentURI);
             }
             ParsedURL pURL = new ParsedURL(uri);
             cssContext.checkLoadExternalResource(pURL, pDocURL);
             
             parseStyleSheet(ss, new InputSource(uri.toString()), uri);
	} catch (SecurityException e) {
            throw e; 
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String s = Messages.formatMessage
                ("syntax.error.at", new Object[] { uri.toString(), m });
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
        }
    }

    /**
     * Parses and creates a new style-sheet.
     * @param rules The style-sheet rules to parse.
     * @param uri The style-sheet URI.
     * @param media The target media of the style-sheet.
     */
    public StyleSheet parseStyleSheet(String rules, URL uri, String media)
        throws DOMException {
        StyleSheet ss = new StyleSheet();
        try {
            ss.setMedia(parser.parseMedia(media));
        } catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String u = ((documentURI == null)?"<unknown>":
                        documentURI.toString());
            String s = Messages.formatMessage
                ("syntax.error.at", new Object[] { u, m });
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
            return ss;
        }
        parseStyleSheet(ss, rules, uri);
        return ss;
    }

    /**
     * Parses and fills the given style-sheet.
     * @param ss The stylesheet to fill.
     * @param rules The style-sheet rules to parse.
     * @param uri The base URI.
     */
    public void parseStyleSheet(StyleSheet ss,
                                String rules,
                                URL uri) throws DOMException {
        try {
            parseStyleSheet(ss, new InputSource(new StringReader(rules)), uri);
	} catch (Exception e) {
            String m = e.getMessage();
            if (m == null) m = "";
            String s = Messages.formatMessage
                ("stylesheet.syntax.error",
                 new Object[] { uri.toString(), rules, m });
            DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
            if (userAgent == null) throw de;
            userAgent.displayError(de);
        }
    }

    /**
     * Parses and fills the given style-sheet.
     * @param ss The stylesheet to fill.
     * @param uri The base URI.
     */
    protected void parseStyleSheet(StyleSheet ss, InputSource is, URL uri)
        throws IOException {
        parser.setSelectorFactory(CSSSelectorFactory.INSTANCE);
        parser.setConditionFactory(cssConditionFactory);
        try {
            cssBaseURI = uri;
            styleSheetDocumentHandler.styleSheet = ss;
            parser.setDocumentHandler(styleSheetDocumentHandler);
            parser.parseStyleSheet(is);

            // Load the imported sheets.
            int len = ss.getSize();
            for (int i = 0; i < len; i++) {
                Rule r = ss.getRule(i);
                if (r.getType() != ImportRule.TYPE) {
                    // @import rules must be the first rules.
                    break;
                }
                ImportRule ir = (ImportRule)r;
                parseStyleSheet(ir, ir.getURI());
            }
        } finally {
            cssBaseURI = null;
        }
    }

    /**
     * Puts an author property from a style-map in another style-map,
     * if possible.
     */
    protected void putAuthorProperty(StyleMap dest,
                                     int idx,
                                     Value sval,
                                     boolean imp,
                                     short origin) {
        Value   dval = dest.getValue(idx);
        short   dorg = dest.getOrigin(idx);
        boolean dimp = dest.isImportant(idx);

        boolean cond = dval == null;
        if (!cond) {
            switch (dorg) {
            case StyleMap.USER_ORIGIN:
                cond = !dimp;
                break;
            case StyleMap.AUTHOR_ORIGIN:
                cond = !dimp || imp;
                break;
            default:
                cond = true;
            }
        }

        if (cond) {
            dest.putValue(idx, sval);
            dest.putImportant(idx, imp);
            dest.putOrigin(idx, origin);
        }
    }

    /**
     * Adds the rules matching the element/pseudo-element of given style
     * sheet to the list.
     */
    protected void addMatchingRules(List rules,
                                    StyleSheet ss,
                                    Element elt,
                                    String pseudo) {
        int len = ss.getSize();
        for (int i = 0; i < len; i++) {
            Rule r = ss.getRule(i);
            switch (r.getType()) {
            case StyleRule.TYPE:
                StyleRule style = (StyleRule)r;
                SelectorList sl = style.getSelectorList();
                int slen = sl.getLength();
                for (int j = 0; j < slen; j++) {
                    ExtendedSelector s = (ExtendedSelector)sl.item(j);
                    if (s.match(elt, pseudo)) {
                        rules.add(style);
                    }
                }
                break;

            case MediaRule.TYPE:
            case ImportRule.TYPE:
                MediaRule mr = (MediaRule)r;
                if (mediaMatch(mr.getMediaList())) {
                    addMatchingRules(rules, mr, elt, pseudo);
                }
                break;
            }
        }
    }

    /**
     * Adds the rules contained in the given list to a stylemap.
     */
    protected void addRules(Element elt,
                            String pseudo,
                            StyleMap sm,
                            List rules,
                            short origin) {
        sortRules(rules, elt, pseudo);
        int rlen = rules.size();

        if (origin == StyleMap.AUTHOR_ORIGIN) {
            for (int r = 0; r < rlen; r++) {
                StyleRule sr = (StyleRule)rules.get(r);
                StyleDeclaration sd = sr.getStyleDeclaration();
                int len = sd.size();
                for (int i = 0; i < len; i++) {
                    putAuthorProperty(sm,
                                      sd.getIndex(i),
                                      sd.getValue(i),
                                      sd.getPriority(i),
                                      origin);
                }
            }
        } else {
            for (int r = 0; r < rlen; r++) {
                StyleRule sr = (StyleRule)rules.get(r);
                StyleDeclaration sd = sr.getStyleDeclaration();
                int len = sd.size();
                for (int i = 0; i < len; i++) {
                    int idx = sd.getIndex(i);
                    sm.putValue(idx, sd.getValue(i));
                    sm.putImportant(idx, sd.getPriority(i));
                    sm.putOrigin(idx, origin);
                }
            }
        }
    }

    /**
     * Sorts the rules matching the element/pseudo-element of given style
     * sheet to the list.
     */
    protected void sortRules(List rules, Element elt, String pseudo) {
        int len = rules.size();
        for (int i = 0; i < len - 1; i++) {
            int idx = i;
            int min = Integer.MAX_VALUE;
            for (int j = i; j < len; j++) {
                StyleRule r = (StyleRule)rules.get(j);
                SelectorList sl = r.getSelectorList();
                int spec = 0;
                int slen = sl.getLength();
                for (int k = 0; k < slen; k++) {
                    ExtendedSelector s = (ExtendedSelector)sl.item(k);
                    if (s.match(elt, pseudo)) {
                        int sp = s.getSpecificity();
                        if (sp > spec) {
                            spec = sp;
                        }
                    }
                }
                if (spec < min) {
                    min = spec;
                    idx = j;
                }
            }
            if (i != idx) {
                Object tmp = rules.get(i);
                rules.set(i, rules.get(idx));
                rules.set(idx, tmp);
            }
        }
    }

    /**
     * Whether the given media list matches the media list of this
     * CSSEngine object.
     */
    protected boolean mediaMatch(SACMediaList ml) {
	if (media == null ||
            ml == null ||
            media.getLength() == 0 ||
            ml.getLength() == 0) {
	    return true;
	}
	for (int i = 0; i < ml.getLength(); i++) {
            if (ml.item(i).equalsIgnoreCase("all"))
                return true;
	    for (int j = 0; j < media.getLength(); j++) {
		if (media.item(j).equalsIgnoreCase("all") ||
                    ml.item(i).equalsIgnoreCase(media.item(j))) {
		    return true;
		}
	    }
	}
	return false;
    }

    /**
     * To parse a style declaration.
     */
    protected class StyleDeclarationDocumentHandler
        extends DocumentAdapter
        implements ShorthandManager.PropertyHandler {
        public StyleMap styleMap;
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#property(String,LexicalUnit,boolean)}.
         */
        public void property(String name, LexicalUnit value, boolean important)
            throws CSSException {
            int i = getPropertyIndex(name);
            if (i == -1) {
                i = getShorthandIndex(name);
                if (i == -1) {
                    // Unknown property
                    return;
                }
                shorthandManagers[i].setValues(CSSEngine.this,
                                               this,
                                               value,
                                               important);
            } else {
                Value v = valueManagers[i].createValue(value, CSSEngine.this);
                putAuthorProperty(styleMap, i, v, important,
                                  StyleMap.INLINE_AUTHOR_ORIGIN);
            }
        }
    }

    /**
     * To build a StyleDeclaration object.
     */
    protected class StyleDeclarationBuilder
        extends DocumentAdapter
        implements ShorthandManager.PropertyHandler {
        public StyleDeclaration styleDeclaration;
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#property(String,LexicalUnit,boolean)}.
         */
        public void property(String name, LexicalUnit value, boolean important)
            throws CSSException {
            int i = getPropertyIndex(name);
            if (i == -1) {
                i = getShorthandIndex(name);
                if (i == -1) {
                    // Unknown property
                    return;
                }
                shorthandManagers[i].setValues(CSSEngine.this,
                                               this,
                                               value,
                                               important);
            } else {
                Value v = valueManagers[i].createValue(value, CSSEngine.this);
                styleDeclaration.append(v, i, important);
            }
        }
    }

    /**
     * To parse a style sheet.
     */
    protected class StyleSheetDocumentHandler
        extends DocumentAdapter
        implements ShorthandManager.PropertyHandler {
        public StyleSheet styleSheet;
        protected StyleRule styleRule;
        protected StyleDeclaration styleDeclaration;

        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#startDocument(InputSource)}.
         */
        public void startDocument(InputSource source)
            throws CSSException {
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#endDocument(InputSource)}.
         */
        public void endDocument(InputSource source) throws CSSException {
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#ignorableAtRule(String)}.
         */
        public void ignorableAtRule(String atRule) throws CSSException {
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#importStyle(String,SACMediaList,String)}.
         */
        public void importStyle(String       uri,
                                SACMediaList media, 
                                String       defaultNamespaceURI)
            throws CSSException {
            ImportRule ir = new ImportRule();
            ir.setMediaList(media);
            ir.setParent(styleSheet);
            try {
                URL base = getCSSBaseURI();
                URL url;
                if (base == null) url = new URL(uri);
                else              url = new URL(base, uri);
                ir.setURI(url);
            } catch (MalformedURLException e) {
            }
            styleSheet.append(ir);
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#startMedia(SACMediaList)}.
         */
        public void startMedia(SACMediaList media) throws CSSException {
            MediaRule mr = new MediaRule();
            mr.setMediaList(media);
            mr.setParent(styleSheet);
            styleSheet.append(mr);
            styleSheet = mr;
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#endMedia(SACMediaList)}.
         */
        public void endMedia(SACMediaList media) throws CSSException {
            styleSheet = styleSheet.getParent();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#startPage(String,String)}.
         */    
        public void startPage(String name, String pseudo_page)
            throws CSSException {
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#endPage(String,String)}.
         */
        public void endPage(String name, String pseudo_page)
            throws CSSException {
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#startFontFace()}.
         */
        public void startFontFace() throws CSSException {
            styleDeclaration = new StyleDeclaration();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#endFontFace()}.
         */
        public void endFontFace() throws CSSException {
            StyleMap sm = new StyleMap(getNumberOfProperties());
            int len = styleDeclaration.size();
            for (int i=0; i<len; i++) {
                int idx = styleDeclaration.getIndex(i);
                sm.putValue(idx, styleDeclaration.getValue(i));
                sm.putImportant(idx, styleDeclaration.getPriority(i));
                // Not sure on this..
                sm.putOrigin(idx, StyleMap.AUTHOR_ORIGIN); 
            }
            styleDeclaration = null;

            int pidx = getPropertyIndex(CSSConstants.CSS_FONT_FAMILY_PROPERTY);
            Value fontFamily = sm.getValue(pidx);
            if (fontFamily == null) return;

            URL base = getCSSBaseURI();
            ParsedURL purl = null;
            if (base != null) purl = new ParsedURL(base);
            fontFaces.add(new FontFaceRule(sm, purl));
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#startSelector(SelectorList)}.
         */
        public void startSelector(SelectorList selectors) throws CSSException {
            styleRule = new StyleRule();
            styleRule.setSelectorList(selectors);
            styleDeclaration = new StyleDeclaration();
            styleRule.setStyleDeclaration(styleDeclaration);
            styleSheet.append(styleRule);
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * org.w3c.css.sac.DocumentHandler#endSelector(SelectorList)}.
         */
        public void endSelector(SelectorList selectors) throws CSSException {
            styleRule = null;
            styleDeclaration = null;
        }

        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#property(String,LexicalUnit,boolean)}.
         */
        public void property(String name, LexicalUnit value, boolean important)
            throws CSSException {
            int i = getPropertyIndex(name);
            if (i == -1) {
                i = getShorthandIndex(name);
                if (i == -1) {
                    // Unknown property
                    return;
                }
                shorthandManagers[i].setValues(CSSEngine.this,
                                               this,
                                               value,
                                               important);
            } else {
                Value v = valueManagers[i].createValue(value, CSSEngine.this);
                styleDeclaration.append(v, i, important);
            }
        }
    }

    /**
     * Provides an adapter for the DocumentHandler interface.
     */
    protected static class DocumentAdapter implements DocumentHandler {

        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#startDocument(InputSource)}.
         */
        public void startDocument(InputSource source)
            throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#endDocument(InputSource)}.
         */
        public void endDocument(InputSource source) throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#comment(String)}.
         */
        public void comment(String text) throws CSSException {
            // We always ignore the comments.
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#ignorableAtRule(String)}.
         */
        public void ignorableAtRule(String atRule) throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#namespaceDeclaration(String,String)}.
         */
        public void namespaceDeclaration(String prefix, String uri) 
            throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#importStyle(String,SACMediaList,String)}.
         */
        public void importStyle(String       uri,
                                SACMediaList media, 
                                String       defaultNamespaceURI)
            throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#startMedia(SACMediaList)}.
         */
        public void startMedia(SACMediaList media) throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#endMedia(SACMediaList)}.
         */
        public void endMedia(SACMediaList media) throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#startPage(String,String)}.
         */    
        public void startPage(String name, String pseudo_page)
            throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#endPage(String,String)}.
         */
        public void endPage(String name, String pseudo_page)
            throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link DocumentHandler#startFontFace()}.
         */
        public void startFontFace() throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link DocumentHandler#endFontFace()}.
         */
        public void endFontFace() throws CSSException {
            throw new InternalError();
        }
        
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#startSelector(SelectorList)}.
         */
        public void startSelector(SelectorList selectors) throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#endSelector(SelectorList)}.
         */
        public void endSelector(SelectorList selectors) throws CSSException {
            throw new InternalError();
        }
    
        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#property(String,LexicalUnit,boolean)}.
         */
        public void property(String name, LexicalUnit value, boolean important)
            throws CSSException {
            throw new InternalError();
        }
    }

    // CSS events /////////////////////////////////////////////////////////
    
    protected final static CSSEngineListener[] LISTENER_ARRAY =
        new CSSEngineListener[0];

    /**
     * Adds a CSS engine listener.
     */
    public void addCSSEngineListener(CSSEngineListener l) {
        listeners.add(l);
    }

    /**
     * Removes a CSS engine listener.
     */
    public void removeCSSEngineListener(CSSEngineListener l) {
        listeners.remove(l);
    }

    /**
     * Fires a CSSEngineEvent, given a list of modified properties.
     */
    protected void firePropertiesChangedEvent(Element target, int[] props) {
        CSSEngineListener[] ll =
            (CSSEngineListener[])listeners.toArray(LISTENER_ARRAY);

        int len = ll.length;
        if (len > 0) {
            CSSEngineEvent evt = new CSSEngineEvent(this, target, props);
            for (int i = 0; i < len; i++) {
                ll[i].propertiesChanged(evt);
            }
        }
    }

    // Dynamic updates ////////////////////////////////////////////////////
    
    /**
     * Called when the inline style of the given element has been updated.
     */
    protected void inlineStyleAttributeUpdated(CSSStylableElement elt,
                                               StyleMap style,
                                               MutationEvent evt) {
        boolean[] updated = styleDeclarationUpdateHandler.updatedProperties;
        for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
            updated[i] = false;
        }

        switch (evt.getAttrChange()) {
        case MutationEvent.ADDITION:
        case MutationEvent.MODIFICATION:
            String decl = evt.getNewValue();
            // System.err.println("Inline Style Update: '" + decl + "'");
            if (decl.length() > 0) {
                element = elt;
                try {
                    parser.setSelectorFactory(CSSSelectorFactory.INSTANCE);
                    parser.setConditionFactory(cssConditionFactory);
                    styleDeclarationUpdateHandler.styleMap = style;
                    parser.setDocumentHandler(styleDeclarationUpdateHandler);
                    parser.parseStyleDeclaration(decl);
                    styleDeclarationUpdateHandler.styleMap = null;
                } catch (Exception e) {
                    String m = e.getMessage();
                    if (m == null) m = "";
                    String u = ((documentURI == null)?"<unknown>":
                                documentURI.toString());
                    String s = Messages.formatMessage
                        ("style.syntax.error.at",
                         new Object[] { u, styleLocalName, decl, m });
                    DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
                    if (userAgent == null) throw de;
                    userAgent.displayError(de);
                } finally {
                    element = null;
                    cssBaseURI = null;
                }
            }

            // Fall through
        case MutationEvent.REMOVAL:
            boolean removed = false;

            if (evt.getPrevValue() != null &&
                evt.getPrevValue().length() > 0) {
                // Check if the style map has cascaded styles which
                // come from the inline style attribute.
                for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
                    if (style.isComputed(i) &&
                        style.getOrigin(i) == StyleMap.INLINE_AUTHOR_ORIGIN &&
                        !updated[i]) {
                        removed = true;
                        updated[i] = true;
                    }
                }
            }

            if (removed) {
                invalidateProperties(elt, null, updated, true);
            } else {
                int count = 0;
                // Invalidate the relative values
                boolean fs = (fontSizeIndex == -1)
                    ? false
                    : updated[fontSizeIndex];
                boolean lh = (lineHeightIndex == -1)
                    ? false
                    : updated[lineHeightIndex];
                boolean cl = (colorIndex == -1)
                    ? false
                    : updated[colorIndex];
                
                for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
                    if (updated[i]) {
                        count++;
                    } 
                    else if ((fs && style.isFontSizeRelative(i)) ||
                             (lh && style.isLineHeightRelative(i)) ||
                             (cl && style.isColorRelative(i))) {
                        updated[i] = true;
                        clearComputedValue(style, i);
                        count++;
                    }
                }

                if (count > 0) {
                    int[] props = new int[count];
                    count = 0;
                    for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
                        if (updated[i]) {
                            props[count++] = i;
                        }
                    }
                    invalidateProperties(elt, props, null, true);
                }
            }
            break;

        default:
            // Must not happen
            throw new InternalError("Invalid attrChangeType");
        }
    }

    private static void clearComputedValue(StyleMap style, int n) {
        if (style.isNullCascaded(n)) {
            style.putValue(n, null);
        } else {
            Value v = style.getValue(n);
            if (v instanceof ComputedValue) {
                ComputedValue cv = (ComputedValue)v;
                v = cv.getCascadedValue();
                style.putValue(n, v);
            }
        }
        style.putComputed(n, false);
    }


    /**
     * Invalidates all the properties of the given node.
     * 
     */
    protected void invalidateProperties(Node node, 
                                        int [] properties, 
                                        boolean [] updated,
                                        boolean recascade) {

        if (!(node instanceof CSSStylableElement))
            return;  // Not Stylable sub tree

        CSSStylableElement elt = (CSSStylableElement)node;
        StyleMap style = elt.getComputedStyleMap(null);
        if (style == null)
            return;  // Nothing to invalidate.
        
        boolean [] diffs = new boolean[getNumberOfProperties()];
        if (updated != null) {
            for (int i=0; i< updated.length; i++) {
                diffs[i] = updated[i];
            }
        }
        if (properties != null) {
            for (int i=0; i<properties.length; i++) {
                diffs[properties[i]] = true;
            }
        }
        int count =0;
        if (!recascade) {
            for (int i=0; i<diffs.length; i++) {
                if (diffs[i])
                    count++;
            }
        } else {
            StyleMap newStyle = getCascadedStyleMap(elt, null);
            elt.setComputedStyleMap(null, newStyle);
            for (int i=0; i<diffs.length; i++) {
                if (diffs[i]) {
                    count++;
                    continue; // Already marked changed.
                }

                // Value nv = getComputedStyle(elt, null, i);
                Value nv = newStyle.getValue(i);
                Value ov = null;
                if (!style.isNullCascaded(i)) {
                    ov = style.getValue(i);
                    if (ov instanceof ComputedValue) {
                        ov = ((ComputedValue)ov).getCascadedValue();
                    }
                }

                if (nv == ov) continue;
                if ((nv != null) && (ov != null)) {
                    if (nv.equals(ov)) continue;
                    String ovCssText = ov.getCssText();
                    String nvCssText = nv.getCssText();
                    if ((nvCssText == ovCssText) ||
                        ((nvCssText != null) && nvCssText.equals(ovCssText)))
                        continue;
                }
                count++;
                diffs[i] = true;
            }
        }
        int []props = null;
        if (count != 0) {
            props = new int[count];
            count = 0;
            for (int i=0; i<diffs.length; i++) {
                if (diffs[i])
                    props[count++] = i;
            }
        }
        propagateChanges(elt, props, recascade);
    }

    /**
     * Propagates the changes that occurs on the parent of the given node.
     * Props is a list of known 'changed' properties.
     * If recascade is true then the stylesheets will be applied
     * again to see if the any new rules apply (or old rules don't
     * apply).
     */
    protected void propagateChanges(Node node, int[] props, 
                                    boolean recascade) {
        if (!(node instanceof CSSStylableElement))
            return;
        CSSStylableElement elt = (CSSStylableElement)node;
        StyleMap style = elt.getComputedStyleMap(null);
        if (style != null) {
            boolean[] updated =
                styleDeclarationUpdateHandler.updatedProperties;
            for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
                updated[i] = false;
            }
            if (props != null) {
                for (int i = props.length - 1; i >= 0; --i) {
                    int idx = props[i];
                    updated[idx] = true;
                }
            }

            // Invalidate the relative values
            boolean fs = (fontSizeIndex == -1)
                ? false
                : updated[fontSizeIndex];
            boolean lh = (lineHeightIndex == -1)
                ? false
                : updated[lineHeightIndex];
            boolean cl = (colorIndex == -1)
                ? false
                : updated[colorIndex];

            int count = 0;
            for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
                if (updated[i]) {
                    count++;
                }
                else if ((fs && style.isFontSizeRelative(i)) ||
                         (lh && style.isLineHeightRelative(i)) ||
                         (cl && style.isColorRelative(i))) {
                    updated[i] = true;
                    clearComputedValue(style, i);
                    count++;
                }
            }

            if (count == 0) {
                props = null;
            } else {
                props = new int[count];
                count = 0;
                for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
                    if (updated[i]) {
                        props[count++] = i;
                    }
                }
                firePropertiesChangedEvent(elt, props);
            }
        }

        int [] inherited = props;
        if (props != null) {
            // Filter out uninheritable properties when we 
            // propogate to children.
            int count = 0;
            for (int i=0; i<props.length; i++) {
                ValueManager vm = valueManagers[props[i]];
                if (vm.isInheritedProperty()) count++;
                else props[i] = -1;
            }
            
            if (count == 0) {
                // nothing to propogate for sure
                inherited = null;
            } else {
                inherited = new int[count];
                count=0;
                for (int i=0; i<props.length; i++)
                    if (props[i] != -1)
                        inherited[count++] = props[i];
            }
        }

        CSSImportedElementRoot ier = getImportedChild(node);
        if (ier != null) {
            Node c = ier.getFirstChild();
            // Don't recascade trees that have been imported.
            // If you do it will use the stylesheets from this
            // document instead of the original document.  Also
            // currently there isn't any supported way to modify
            // the content imported from the other document so
            // the cascade can't change.
            invalidateProperties(c, inherited, null, ier.getIsLocal());
        }
        for (Node n = node.getFirstChild();
             n != null;
             n = n.getNextSibling()) {
            invalidateProperties(n, inherited, null, recascade);
        }
    }

    /**
     * To parse a style declaration and update a StyleMap.
     */
    protected class StyleDeclarationUpdateHandler
        extends DocumentAdapter
        implements ShorthandManager.PropertyHandler {
        public StyleMap styleMap;
        public boolean[] updatedProperties =
            new boolean[getNumberOfProperties()];

        /**
         * <b>SAC</b>: Implements {@link
         * DocumentHandler#property(String,LexicalUnit,boolean)}.
         */
        public void property(String name, LexicalUnit value, boolean important)
            throws CSSException {
            int i = getPropertyIndex(name);
            if (i == -1) {
                i = getShorthandIndex(name);
                if (i == -1) {
                    // Unknown property
                    return;
                }
                shorthandManagers[i].setValues(CSSEngine.this,
                                               this,
                                               value,
                                               important);
            } else {
                if (styleMap.isImportant(i)) {
                    // The previous value is important, and a value
                    // from a style attribute cannot be important...
                    return;
                }

                updatedProperties[i] = true;

                Value v = valueManagers[i].createValue(value, CSSEngine.this);
                styleMap.putMask(i, (short)0);
                styleMap.putValue(i, v);
                styleMap.putOrigin(i, StyleMap.INLINE_AUTHOR_ORIGIN);
            }
        }
    }

    /**
     * Called when a non-CSS presentational hint has been updated.
     */
    protected void nonCSSPresentationalHintUpdated(CSSStylableElement elt,
                                                   StyleMap style,
                                                   String property,
                                                   MutationEvent evt) {
        // System.err.println("update: " + property);
        int idx = getPropertyIndex(property);

        if (style.isImportant(idx)) {
            // The current value is important, and a value
            // from an XML attribute cannot be important...
            return;
        }

        switch (style.getOrigin(idx)) {
        case StyleMap.AUTHOR_ORIGIN:
        case StyleMap.INLINE_AUTHOR_ORIGIN:
            // The current value has a greater priority
            return;
        }
        
        switch (evt.getAttrChange()) {
        case MutationEvent.ADDITION:
        case MutationEvent.MODIFICATION:
            element = elt;
            try {
                LexicalUnit lu;
                lu = parser.parsePropertyValue(evt.getNewValue());
                ValueManager vm = valueManagers[idx];
                Value v = vm.createValue(lu, CSSEngine.this);
                style.putMask(idx, (short)0);
                style.putValue(idx, v);
                style.putOrigin(idx, StyleMap.NON_CSS_ORIGIN);
            } catch (Exception e) {
                String m = e.getMessage();
                if (m == null) m = "";
                String u = ((documentURI == null)?"<unknown>":
                            documentURI.toString());
                String s = Messages.formatMessage
                    ("property.syntax.error.at",
                     new Object[] { u, property, evt.getNewValue(), m });
                DOMException de = new DOMException(DOMException.SYNTAX_ERR, s);
                if (userAgent == null) throw de;
                userAgent.displayError(de);
            } finally {
                element = null;
                cssBaseURI = null;
            }
            break;

        case MutationEvent.REMOVAL: 
            {
                int [] invalid = { idx };
                invalidateProperties(elt, invalid, null, true);
                return;
            }
        }

        boolean[] updated = styleDeclarationUpdateHandler.updatedProperties;
        for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
            updated[i] = false;
        }
        updated[idx] = true;

        // Invalidate the relative values
        boolean fs = idx == fontSizeIndex;
        boolean lh = idx == lineHeightIndex;
        boolean cl = idx == colorIndex;
        int count = 0;

        for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
            if (updated[i]) {
                count++;
            }
            else if ((fs && style.isFontSizeRelative(i)) ||
                     (lh && style.isLineHeightRelative(i)) ||
                     (cl && style.isColorRelative(i))) {
                updated[i] = true;
                clearComputedValue(style, i);
                count++;
            }
        }

        int[] props = new int[count];
        count = 0;
        for (int i = getNumberOfProperties() - 1; i >= 0; --i) {
            if (updated[i]) {
                props[count++] = i;
            }
        }

        invalidateProperties(elt, props, null, true);
    }

    /**
     * To handle the insertion of a CSSStyleSheetNode in the
     * associated document.
     */
    protected class DOMNodeInsertedListener implements EventListener {
        public void handleEvent(Event evt) {
            EventTarget et = evt.getTarget();
            if (et instanceof CSSStyleSheetNode) {
                styleSheetNodes = null;
                // Invalidate all the CSSStylableElements in the document.
                invalidateProperties(document.getDocumentElement(), 
                                     null, null, true);
                return;
            }
            if (et instanceof CSSStylableElement) {
                // Invalidate the CSSStylableElement siblings, to
                // correctly match the adjacent selectors and
                // first-child pseudo-class.
                for (Node n = ((Node)evt.getTarget()).getNextSibling();
                     n != null;
                     n = n.getNextSibling()) {
                    invalidateProperties(n, null, null, true);
                }
            }
        }
    }

    /**
     * To handle the removal of a CSSStyleSheetNode from the
     * associated document.
     */
    protected class DOMNodeRemovedListener implements EventListener {
        public void handleEvent(Event evt) {
            EventTarget et = evt.getTarget();
            if (et instanceof CSSStyleSheetNode) {
                // Wait for the DOMSubtreeModified to do the invalidations
                // because at this time the node is in the tree.
                styleSheetRemoved = true;
            } else if (et instanceof CSSStylableElement) {
                // Wait for the DOMSubtreeModified to do the invalidations
                // because at this time the node is in the tree.
                removedStylableElementSibling = ((Node)et).getNextSibling();
            }
            // Clears the computed styles in the removed tree.
            disposeStyleMaps((Node)et);
        }
    }

    /**
     * To handle the removal of a CSSStyleSheetNode from the
     * associated document.
     */
    protected class DOMSubtreeModifiedListener implements EventListener {
        public void handleEvent(Event evt) {
            if (styleSheetRemoved) {
                styleSheetRemoved = false;
                styleSheetNodes = null;

                // Invalidate all the CSSStylableElements in the document.
                invalidateProperties(document.getDocumentElement(), 
                                     null, null, true);
            } else if (removedStylableElementSibling != null) {
                // Invalidate the CSSStylableElement siblings, to
                // correctly match the adjacent selectors and
                // first-child pseudo-class.
                for (Node n = removedStylableElementSibling;
                     n != null;
                     n = n.getNextSibling()) {
                    invalidateProperties(n, null, null, true);
                }
                removedStylableElementSibling = null;
            }
        }
    }

    /**
     * To handle the modification of a CSSStyleSheetNode.
     */
    protected class DOMCharacterDataModifiedListener implements EventListener {
        public void handleEvent(Event evt) {
            Node n = (Node)evt.getTarget();
            if (n.getParentNode() instanceof CSSStyleSheetNode) {
                styleSheetNodes = null;
                // Invalidate all the CSSStylableElements in the document.
                invalidateProperties(document.getDocumentElement(), 
                                     null, null, true);
            }
        }
    }

    /**
     * To handle the element attributes modification in the associated
     * document.
     */
    protected class DOMAttrModifiedListener implements EventListener {
        public void handleEvent(Event evt) {
            EventTarget et = evt.getTarget();
            if (!(et instanceof CSSStylableElement)) {
                // Not a stylable element.
                return;
            }

            MutationEvent mevt = (MutationEvent)evt;
            if (mevt.getNewValue().equals(mevt.getPrevValue()))
                return;  // no change really...

            Node attr = mevt.getRelatedNode();
            String attrNS = attr.getNamespaceURI();
            String name = (attrNS == null)
                ? attr.getNodeName()
                : attr.getLocalName();
                
            CSSStylableElement elt = (CSSStylableElement)et;
            StyleMap style = elt.getComputedStyleMap(null);
            if (style != null) {
                if ((attrNS == null && styleNamespaceURI == null) ||
                    (attrNS != null && attrNS.equals(styleNamespaceURI))) {
                    if (name.equals(styleLocalName)) {
                        // The style declaration attribute has been modified.
                        inlineStyleAttributeUpdated(elt, style, mevt);
                        return;
                    }
                }

                if (nonCSSPresentationalHints != null) {
                    if ((attrNS == null &&
                         nonCSSPresentationalHintsNamespaceURI == null) ||
                        (attrNS != null &&
                         attrNS.equals(nonCSSPresentationalHintsNamespaceURI))) {
                        if (nonCSSPresentationalHints.contains(name)) {
                            // The 'name' attribute which represents a non CSS
                            // presentational hint has been modified.
                            nonCSSPresentationalHintUpdated(elt, style, name,
                                                            mevt);
                            return;
                        }
                    }
                }
            }

            if (selectorAttributes != null &&
                selectorAttributes.contains(name)) {
                // An attribute has been modified, invalidate all the
                // properties to correctly match attribute selectors.
                invalidateProperties(elt, null, null, true);
                for (Node n = elt.getNextSibling();
                     n != null;
                     n = n.getNextSibling()) {
                    invalidateProperties(n, null, null, true);
                }
            }
        }
    }
}
