/*

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

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

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

 */
package org.apache.batik.bridge;

import java.awt.Color;
import java.awt.Paint;
import java.lang.ref.WeakReference;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Arrays;
import java.util.Set;

import org.apache.batik.anim.AnimationEngine;
import org.apache.batik.anim.AnimationException;
import org.apache.batik.anim.dom.AnimationTarget;
import org.apache.batik.anim.dom.SVGOMDocument;
import org.apache.batik.anim.dom.SVGOMElement;
import org.apache.batik.anim.dom.SVGStylableElement;
import org.apache.batik.anim.timing.TimedDocumentRoot;
import org.apache.batik.anim.timing.TimedElement;
import org.apache.batik.anim.values.AnimatableAngleValue;
import org.apache.batik.anim.values.AnimatableAngleOrIdentValue;
import org.apache.batik.anim.values.AnimatableBooleanValue;
import org.apache.batik.anim.values.AnimatableIntegerValue;
import org.apache.batik.anim.values.AnimatableLengthValue;
import org.apache.batik.anim.values.AnimatableLengthListValue;
import org.apache.batik.anim.values.AnimatableLengthOrIdentValue;
import org.apache.batik.anim.values.AnimatableNumberValue;
import org.apache.batik.anim.values.AnimatableNumberListValue;
import org.apache.batik.anim.values.AnimatableNumberOrPercentageValue;
import org.apache.batik.anim.values.AnimatablePathDataValue;
import org.apache.batik.anim.values.AnimatablePointListValue;
import org.apache.batik.anim.values.AnimatablePreserveAspectRatioValue;
import org.apache.batik.anim.values.AnimatableNumberOrIdentValue;
import org.apache.batik.anim.values.AnimatableRectValue;
import org.apache.batik.anim.values.AnimatableStringValue;
import org.apache.batik.anim.values.AnimatableValue;
import org.apache.batik.anim.values.AnimatableColorValue;
import org.apache.batik.anim.values.AnimatablePaintValue;
import org.apache.batik.css.engine.CSSEngine;
import org.apache.batik.css.engine.CSSStylableElement;
import org.apache.batik.css.engine.StyleMap;
import org.apache.batik.css.engine.value.FloatValue;
import org.apache.batik.css.engine.value.StringValue;
import org.apache.batik.css.engine.value.Value;
import org.apache.batik.css.engine.value.ValueManager;
import org.apache.batik.parser.DefaultPreserveAspectRatioHandler;
import org.apache.batik.parser.FloatArrayProducer;
import org.apache.batik.parser.DefaultLengthHandler;
import org.apache.batik.parser.LengthArrayProducer;
import org.apache.batik.parser.LengthHandler;
import org.apache.batik.parser.LengthListParser;
import org.apache.batik.parser.LengthParser;
import org.apache.batik.parser.NumberListParser;
import org.apache.batik.parser.PathArrayProducer;
import org.apache.batik.parser.PathParser;
import org.apache.batik.parser.PointsParser;
import org.apache.batik.parser.ParseException;
import org.apache.batik.parser.PreserveAspectRatioHandler;
import org.apache.batik.parser.PreserveAspectRatioParser;
import org.apache.batik.util.RunnableQueue;
import org.apache.batik.util.SMILConstants;
import org.apache.batik.constants.XMLConstants;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.css.CSSPrimitiveValue;
import org.w3c.dom.css.CSSStyleDeclaration;
import org.w3c.dom.css.CSSValue;
import org.w3c.dom.events.EventTarget;
import org.w3c.dom.svg.SVGAngle;
import org.w3c.dom.svg.SVGLength;
import org.w3c.dom.svg.SVGPreserveAspectRatio;

/**
 * An AnimationEngine for SVG documents.
 *
 * @author <a href="mailto:cam%40mcc%2eid%2eau">Cameron McCormack</a>
 * @version $Id: SVGAnimationEngine.java 1851346 2019-01-15 13:41:00Z ssteiner $
 */
public class SVGAnimationEngine extends AnimationEngine {

    /**
     * The BridgeContext to use for value parsing.
     */
    protected BridgeContext ctx;

    /**
     * The CSSEngine used for CSS value parsing.
     */
    protected CSSEngine cssEngine;

    /**
     * Whether animation processing has started.  This affects whether
     * animation element bridges add their animation on to the initial
     * bridge list, or process them immediately.
     */
    protected boolean started;

    /**
     * The Runnable that ticks the document.
     */
    protected AnimationTickRunnable animationTickRunnable;

    /**
     * The initial time that will be seeked to when the animation engine starts,
     * as set by {@link #setCurrentTime}.
     */
    protected float initialStartTime;

    /**
     * The factory for unparsed string values.
     */
    protected UncomputedAnimatableStringValueFactory
        uncomputedAnimatableStringValueFactory =
            new UncomputedAnimatableStringValueFactory();

    /**
     * The factory for length-or-ident values.
     */
    protected AnimatableLengthOrIdentFactory
        animatableLengthOrIdentFactory = new AnimatableLengthOrIdentFactory();

    /**
     * The factory for number-or-ident values.
     */
    protected AnimatableNumberOrIdentFactory
        animatableNumberOrIdentFactory =
            new AnimatableNumberOrIdentFactory(false);

    /**
     * Factories for {@link AnimatableValue} parsing.
     */
    protected Factory[] factories = {
        null, // TYPE_UNKNOWN
            new AnimatableIntegerValueFactory(), // TYPE_INTEGER
            new AnimatableNumberValueFactory(), // TYPE_NUMBER
            new AnimatableLengthValueFactory(), // TYPE_LENGTH
        null, // TYPE_NUMBER_OPTIONAL_NUMBER
        new AnimatableAngleValueFactory(), // TYPE_ANGLE
        new AnimatableColorValueFactory(), // TYPE_COLOR
        new AnimatablePaintValueFactory(), // TYPE_PAINT
        null, // TYPE_PERCENTAGE
        null, // TYPE_TRANSFORM_LIST
        uncomputedAnimatableStringValueFactory, // TYPE_URI
        null, // TYPE_FREQUENCY
        null, // TYPE_TIME
            new AnimatableNumberListValueFactory(), // TYPE_NUMBER_LIST
            new AnimatableLengthListValueFactory(), // TYPE_LENGTH_LIST
        uncomputedAnimatableStringValueFactory, // TYPE_IDENT
        uncomputedAnimatableStringValueFactory, // TYPE_CDATA
        animatableLengthOrIdentFactory, // TYPE_LENGTH_OR_INHERIT
        uncomputedAnimatableStringValueFactory, // TYPE_IDENT_LIST
        uncomputedAnimatableStringValueFactory, // TYPE_CLIP_VALUE
        uncomputedAnimatableStringValueFactory, // TYPE_URI_OR_IDENT
        uncomputedAnimatableStringValueFactory, // TYPE_CURSOR_VALUE
            new AnimatablePathDataFactory(), // TYPE_PATH_DATA
        uncomputedAnimatableStringValueFactory, // TYPE_ENABLE_BACKGROUND_VALUE
        null, // TYPE_TIME_VALUE_LIST
        animatableNumberOrIdentFactory, // TYPE_NUMBER_OR_INHERIT
        uncomputedAnimatableStringValueFactory, // TYPE_FONT_FAMILY_VALUE
        null, // TYPE_FONT_FACE_FONT_SIZE_VALUE
        new AnimatableNumberOrIdentFactory(true), // TYPE_FONT_WEIGHT_VALUE
        new AnimatableAngleOrIdentFactory(), // TYPE_ANGLE_OR_IDENT
        null, // TYPE_KEY_SPLINES_VALUE
            new AnimatablePointListValueFactory(), // TYPE_POINTS_VALUE
            new AnimatablePreserveAspectRatioValueFactory(), // TYPE_PRESERVE_ASPECT_RATIO_VALUE
        null, // TYPE_URI_LIST
        uncomputedAnimatableStringValueFactory, // TYPE_LENGTH_LIST_OR_IDENT
        null, // TYPE_CHARACTER_OR_UNICODE_RANGE_LIST
        null, // TYPE_UNICODE_RANGE_LIST
        null, // TYPE_FONT_VALUE
        null, // TYPE_FONT_DECSRIPTOR_SRC_VALUE
        animatableLengthOrIdentFactory, // TYPE_FONT_SIZE_VALUE
        animatableLengthOrIdentFactory, // TYPE_BASELINE_SHIFT_VALUE
        animatableLengthOrIdentFactory, // TYPE_KERNING_VALUE
        animatableLengthOrIdentFactory, // TYPE_SPACING_VALUE
        animatableLengthOrIdentFactory, // TYPE_LINE_HEIGHT_VALUE
        animatableNumberOrIdentFactory, // TYPE_FONT_SIZE_ADJUST_VALUE
        null, // TYPE_LANG_VALUE
        null, // TYPE_LANG_LIST_VALUE
            new AnimatableNumberOrPercentageValueFactory(), // TYPE_NUMBER_OR_PERCENTAGE
        null, // TYPE_TIMING_SPECIFIER_LIST
            new AnimatableBooleanValueFactory(), // TYPE_BOOLEAN
            new AnimatableRectValueFactory() // TYPE_RECT
    };

    /**
     * Whether the document is an SVG 1.2 document.
     */
    protected boolean isSVG12;

    /**
     * List of bridges that will be initialized when the document is started.
     */
    protected LinkedList initialBridges = new LinkedList();

    /**
     * A StyleMap used by the {@link Factory}s when computing CSS values.
     */
    protected StyleMap dummyStyleMap;

    /**
     * The thread that ticks the animation engine.
     */
    protected AnimationThread animationThread;

    /**
     * The animation limiting mode.
     */
    protected int animationLimitingMode;

    /**
     * The amount of animation limiting.
     */
    protected float animationLimitingAmount;

    /**
     * Set of SMIL animation event names for SVG 1.1.
     */
    protected static final Set animationEventNames11 = new HashSet();

    /**
     * Set of SMIL animation event names for SVG 1.2.
     */
    protected static final Set animationEventNames12 = new HashSet();

    static {
        String[] eventNamesCommon = {
            "click", "mousedown", "mouseup", "mouseover", "mousemove",
            "mouseout", "beginEvent", "endEvent"
        };
        String[] eventNamesSVG11 = {
            "DOMSubtreeModified", "DOMNodeInserted", "DOMNodeRemoved",
            "DOMNodeRemovedFromDocument", "DOMNodeInsertedIntoDocument",
            "DOMAttrModified", "DOMCharacterDataModified", "SVGLoad",
            "SVGUnload", "SVGAbort", "SVGError", "SVGResize", "SVGScroll",
            "repeatEvent"
        };
        String[] eventNamesSVG12 = {
            "load", "resize", "scroll", "zoom"
        };
        for (String anEventNamesCommon : eventNamesCommon) {
            animationEventNames11.add(anEventNamesCommon);
            animationEventNames12.add(anEventNamesCommon);
        }
        for (String anEventNamesSVG11 : eventNamesSVG11) {
            animationEventNames11.add(anEventNamesSVG11);
        }
        for (String anEventNamesSVG12 : eventNamesSVG12) {
            animationEventNames12.add(anEventNamesSVG12);
        }
    }

    /**
     * Creates a new SVGAnimationEngine.
     */
    public SVGAnimationEngine(Document doc, BridgeContext ctx) {
        super(doc);
        this.ctx = ctx;
        SVGOMDocument d = (SVGOMDocument) doc;
        cssEngine = d.getCSSEngine();
        dummyStyleMap = new StyleMap(cssEngine.getNumberOfProperties());
        isSVG12 = d.isSVG12();
    }

    /**
     * Disposes this animation engine.
     */
    public void dispose() {
        synchronized (this) {
            pause();
            super.dispose();
        }
    }

    /**
     * Adds an animation element bridge to the list of bridges that
     * require initializing when the document is started.
     */
    public void addInitialBridge(SVGAnimationElementBridge b) {
        if (initialBridges != null) {
            initialBridges.add(b);
        }
    }

    /**
     * Returns whether animation processing has begun.
     */
    public boolean hasStarted() {
        return started;
    }

    /**
     * Parses an AnimatableValue.
     */
    public AnimatableValue parseAnimatableValue(Element animElt,
                                                AnimationTarget target,
                                                String ns, String ln,
                                                boolean isCSS,
                                                String s) {
        SVGOMElement elt = (SVGOMElement) target.getElement();
        int type;
        if (isCSS) {
            type = elt.getPropertyType(ln);
        } else {
            type = elt.getAttributeType(ns, ln);
        }
        Factory factory = factories[type];
        if (factory == null) {
            String an = ns == null ? ln : '{' + ns + '}' + ln;
            throw new BridgeException
                (ctx, animElt, "attribute.not.animatable",
                 new Object[] { target.getElement().getNodeName(), an });
        }
        return factories[type].createValue(target, ns, ln, isCSS, s);
    }

    /**
     * Returns an AnimatableValue for the underlying value of a CSS property.
     */
    public AnimatableValue getUnderlyingCSSValue(Element animElt,
                                                 AnimationTarget target,
                                                 String pn) {
        ValueManager[] vms = cssEngine.getValueManagers();
        int idx = cssEngine.getPropertyIndex(pn);
        if (idx != -1) {
            int type = vms[idx].getPropertyType();
            Factory factory = factories[type];
            if (factory == null) {
                throw new BridgeException
                    (ctx, animElt, "attribute.not.animatable",
                     new Object[] { target.getElement().getNodeName(), pn });
            }
            SVGStylableElement e = (SVGStylableElement) target.getElement();
            CSSStyleDeclaration over = e.getOverrideStyle();
            String oldValue = over.getPropertyValue(pn);
            if (oldValue != null) {
                over.removeProperty(pn);
            }
            Value v = cssEngine.getComputedStyle(e, null, idx);
            if (oldValue != null && !oldValue.equals("")) {
                over.setProperty(pn, oldValue, null);
            }
            return factories[type].createValue(target, pn, v);
        }
        // XXX Doesn't handle shorthands.
        return null;
    }

    /**
     * Pauses the animations.
     */
    public void pause() {
        super.pause();
        UpdateManager um = ctx.getUpdateManager();
        if (um != null) {
            um.getUpdateRunnableQueue().setIdleRunnable(null);
        }
    }

    /**
     * Pauses the animations.
     */
    public void unpause() {
        super.unpause();
        UpdateManager um = ctx.getUpdateManager();
        if (um != null) {
            um.getUpdateRunnableQueue().setIdleRunnable(animationTickRunnable);
        }
    }

    /**
     * Returns the current document time.
     */
    public float getCurrentTime() {
        boolean p = pauseTime != 0;
        unpause();
        float t = timedDocumentRoot.getCurrentTime();
        if (p) {
            pause();
        }
        return Float.isNaN(t) ? 0 : t;
    }

    /**
     * Sets the current document time.
     */
    public float setCurrentTime(float t) {
        if (started) {
            float ret = super.setCurrentTime(t);
            if (animationTickRunnable != null) {
                animationTickRunnable.resume();
            }
            return ret;
        } else {
            initialStartTime = t;
            return 0;
        }
    }

    /**
     * Creates a new returns a new TimedDocumentRoot object for the document.
     */
    protected TimedDocumentRoot createDocumentRoot() {
        return new AnimationRoot();
    }

    /**
     * Starts the animation engine.
     */
    public void start(long documentStartTime) {
        if (started) {
            return;
        }
        started = true;
        try {
            try {
                Calendar cal = Calendar.getInstance();
                cal.setTime(new Date(documentStartTime));
                timedDocumentRoot.resetDocument(cal);
                Object[] bridges = initialBridges.toArray();
                initialBridges = null;
                for (Object bridge2 : bridges) {
                    SVGAnimationElementBridge bridge =
                            (SVGAnimationElementBridge) bridge2;
                    bridge.initializeAnimation();
                }
                for (Object bridge1 : bridges) {
                    SVGAnimationElementBridge bridge =
                            (SVGAnimationElementBridge) bridge1;
                    bridge.initializeTimedElement();
                }

                // tick(0, false);
                // animationThread = new AnimationThread();
                // animationThread.start();
                UpdateManager um = ctx.getUpdateManager();
                if (um != null) {
                    RunnableQueue q = um.getUpdateRunnableQueue();
                    animationTickRunnable = new AnimationTickRunnable(q, this);
                    q.setIdleRunnable(animationTickRunnable);
                    if (initialStartTime != 0) {
                        setCurrentTime(initialStartTime);
                    }
                }
            } catch (AnimationException ex) {
                throw new BridgeException(ctx, ex.getElement().getElement(),
                                          ex.getMessage());
            }
        } catch (Exception ex) {
            if (ctx.getUserAgent() == null) {
                ex.printStackTrace();
            } else {
                ctx.getUserAgent().displayError(ex);
            }
        }
    }

    /**
     * Sets the animation limiting mode to "none".
     */
    public void setAnimationLimitingNone() {
        animationLimitingMode = 0;
    }

    /**
     * Sets the animation limiting mode to a percentage of CPU.
     * @param pc the maximum percentage of CPU to use (0 &lt; pc ≤ 1)
     */
    public void setAnimationLimitingCPU(float pc) {
        animationLimitingMode = 1;
        animationLimitingAmount = pc;
    }

    /**
     * Sets the animation limiting mode to a number of frames per second.
     * @param fps the maximum number of frames per second (fps &gt; 0)
     */
    public void setAnimationLimitingFPS(float fps) {
        animationLimitingMode = 2;
        animationLimitingAmount = fps;
    }

    /**
     * A class for the root time container.
     */
    protected class AnimationRoot extends TimedDocumentRoot {

        /**
         * Creates a new AnimationRoot object.
         */
        public AnimationRoot() {
            super(!isSVG12, isSVG12);
        }

        /**
         * Returns the namespace URI of the event that corresponds to the given
         * animation event name.
         */
        protected String getEventNamespaceURI(String eventName) {
            if (!isSVG12) {
                return null;
            }
            if (eventName.equals("focusin")
                    || eventName.equals("focusout")
                    || eventName.equals("activate")
                    || animationEventNames12.contains(eventName)) {
                return XMLConstants.XML_EVENTS_NAMESPACE_URI;
            }
            return null;
        }

        /**
         * Returns the type of the event that corresponds to the given
         * animation event name.
         */
        protected String getEventType(String eventName) {
            if (eventName.equals("focusin")) {
                return "DOMFocusIn";
            } else if (eventName.equals("focusout")) {
                return "DOMFocusOut";
            } else if (eventName.equals("activate")) {
                return "DOMActivate";
            }
            if (isSVG12) {
                if (animationEventNames12.contains(eventName)) {
                    return eventName;
                }
            } else {
                if (animationEventNames11.contains(eventName)) {
                    return eventName;
                }
            }
            return null;
        }

        /**
         * Returns the name of the repeat event.
         * @return "repeatEvent" for SVG
         */
        protected String getRepeatEventName() {
            return SMILConstants.SMIL_REPEAT_EVENT_NAME;
        }

        /**
         * Fires a TimeEvent of the given type on this element.
         * @param eventType the type of TimeEvent ("beginEvent", "endEvent"
         *                  or "repeatEvent"/"repeat").
         * @param time the timestamp of the event object
         */
        protected void fireTimeEvent(String eventType, Calendar time,
                                     int detail) {
            AnimationSupport.fireTimeEvent
                ((EventTarget) document, eventType, time, detail);
        }

        /**
         * Invoked to indicate this timed element became active at the
         * specified time.
         * @param begin the time the element became active, in document simple time
         */
        protected void toActive(float begin) {
        }

        /**
         * Invoked to indicate that this timed element became inactive.
         * @param stillActive if true, indicates that the element is still
         *                    actually active, but between the end of the
         *                    computed repeat duration and the end of the
         *                    interval
         * @param isFrozen whether the element is frozen or not
         */
        protected void toInactive(boolean stillActive, boolean isFrozen) {
        }

        /**
         * Invoked to indicate that this timed element has had its fill removed.
         */
        protected void removeFill() {
        }

        /**
         * Invoked to indicate that this timed element has been sampled at the
         * given time.
         * @param simpleTime the sample time in local simple time
         * @param simpleDur the simple duration of the element
         * @param repeatIteration the repeat iteration during which the element
         *                        was sampled
         */
        protected void sampledAt(float simpleTime, float simpleDur,
                                 int repeatIteration) {
        }

        /**
         * Invoked to indicate that this timed element has been sampled
         * at the end of its active time, at an integer multiple of the
         * simple duration.  This is the "last" value that will be used
         * for filling, which cannot be sampled normally.
         */
        protected void sampledLastValue(int repeatIteration) {
        }

        /**
         * Returns the timed element with the given ID.
         */
        protected TimedElement getTimedElementById(String id) {
            return AnimationSupport.getTimedElementById(id, document);
        }

        /**
         * Returns the event target with the given ID.
         */
        protected EventTarget getEventTargetById(String id) {
            return AnimationSupport.getEventTargetById(id, document);
        }

        /**
         * Returns the target of this animation as an {@link EventTarget}.  Used
         * for eventbase timing specifiers where the element ID is omitted.
         */
        protected EventTarget getAnimationEventTarget() {
            return null;
        }

        /**
         * Returns the event target that should be listened to for
         * access key events.
         */
        protected EventTarget getRootEventTarget() {
            return (EventTarget) document;
        }

        /**
         * Returns the DOM element that corresponds to this timed element, if
         * such a DOM element exists.
         */
        public Element getElement() {
            return null;
        }

        /**
         * Returns whether this timed element comes before the given timed
         * element in document order.
         */
        public boolean isBefore(TimedElement other) {
            return false;
        }

        /**
         * Invoked by timed elements in this document to indicate that the
         * current interval will be re-evaluated at the next sample.
         */
        protected void currentIntervalWillUpdate() {
            if (animationTickRunnable != null) {
                animationTickRunnable.resume();
            }
        }
    }

    /**
     * Idle runnable to tick the animation, that reads times from System.in.
     */
    protected static class DebugAnimationTickRunnable extends AnimationTickRunnable {

        float t = 0f;

        public DebugAnimationTickRunnable(RunnableQueue q, SVGAnimationEngine eng) {
            super(q, eng);
            waitTime = Long.MAX_VALUE;
            new Thread() {
                public void run() {
                    java.io.BufferedReader r = new java.io.BufferedReader(new java.io.InputStreamReader(System.in));
                    System.out.println("Enter times.");
                    for (;;) {
                        String s;
                        try {
                            s = r.readLine();
                        } catch (java.io.IOException e) {
                            s = null;
                        }
                        if (s == null) {
                            System.exit(0);
                        }
                        t = Float.parseFloat(s);
                        DebugAnimationTickRunnable.this.resume();
                    }
                }
            }.start();
        }

        public void resume() {
            waitTime = 0;
            Object lock = q.getIteratorLock();
            synchronized (lock) {
                lock.notify();
            }
        }

        public long getWaitTime() {
            long wt = waitTime;
            waitTime = Long.MAX_VALUE;
            return wt;
        }

        public void run() {
            SVGAnimationEngine eng = getAnimationEngine();
            synchronized (eng) {
                try {
                    try {
                        eng.tick(t, false);
                    } catch (AnimationException ex) {
                        throw new BridgeException
                            (eng.ctx, ex.getElement().getElement(),
                             ex.getMessage());
                    }
                } catch (Exception ex) {
                    if (eng.ctx.getUserAgent() == null) {
                        ex.printStackTrace();
                    } else {
                        eng.ctx.getUserAgent().displayError(ex);
                    }
                }
            }
        }
    }

    /**
     * Idle runnable to tick the animation.
     */
    protected static class AnimationTickRunnable
            implements RunnableQueue.IdleRunnable {

        /**
         * Calendar instance used for passing current time values to the
         * animation timing system.
         */
        protected Calendar time = Calendar.getInstance();

//         /**
//          * The current document time in seconds, truncated.
//          */
//         protected double second = -1.;

//         /**
//          * The number of frames that have been ticked so far this second.
//          */
//         protected int frames;

        /**
         * The number of milliseconds to wait until the next animation tick.
         * This is returned by {@link #getWaitTime()}.
         */
        protected long waitTime;

        /**
         * The RunnableQueue in which this is the
         * {@link RunnableQueue.IdleRunnable}.
         */
        protected RunnableQueue q;

        /**
         * The number of past tick times to keep, for computing the average
         * time per tick.
         */
        private static final int NUM_TIMES = 8;

        /**
         * The past tick times.
         */
        protected long[] times = new long[NUM_TIMES];

        /**
         * The sum of the times in {@link #times}.
         */
        protected long sumTime;

        /**
         * The current index into {@link #times}.
         */
        protected int timeIndex;

        /**
         * A weak reference to the SVGAnimationEngine this AnimationTickRunnable
         * is for.  We make this a WeakReference so that a ticking animation
         * engine does not prevent from being GCed.
         */
        protected WeakReference engRef;

        /**
         * The maximum number of consecutive exceptions to allow before
         * stopping the report of them.
         */
        protected static final int MAX_EXCEPTION_COUNT = 10;

        /**
         * The number of consecutive exceptions that have been thrown.  This is
         * used to detect when exceptions are occurring every tick, and to stop
         * reporting them when this happens.
         */
        protected int exceptionCount;

        /**
         * Creates a new AnimationTickRunnable.
         */
        public AnimationTickRunnable(RunnableQueue q, SVGAnimationEngine eng) {
            this.q = q;
            this.engRef = new WeakReference(eng);
            // Initialize the past times to 100ms.
            Arrays.fill(times, 100);
            sumTime = 100 * NUM_TIMES;
        }

        /**
         * Forces an animation update, if the {@link RunnableQueue} is
         * currently waiting.
         */
        public void resume() {
            waitTime = 0;
            Object lock = q.getIteratorLock();
            synchronized (lock) {
                lock.notify();
            }
        }

        /**
         * Returns the system time that can be safely waited until before this
         * {@link Runnable} is run again.
         *
         * @return time to wait until, <code>0</code> if no waiting can
         *         be done, or {@link Long#MAX_VALUE} if the {@link Runnable}
         *         should not be run again at this time
         */
        public long getWaitTime() {
            return waitTime;
        }

        /**
         * Performs one tick of the animation.
         */
        public void run() {
            SVGAnimationEngine eng = getAnimationEngine();
            synchronized (eng) {
                int animationLimitingMode = eng.animationLimitingMode;
                float animationLimitingAmount = eng.animationLimitingAmount;
                try {
                    try {
                        long before = System.currentTimeMillis();
                        time.setTime(new Date(before));
                        float t = eng.timedDocumentRoot.convertWallclockTime(time);
//                         if (Math.floor(t) > second) {
//                             second = Math.floor(t);
//                             System.err.println("fps: " + frames);
//                             frames = 0;
//                         }
                        float t2 = eng.tick(t, false);
                        long after = System.currentTimeMillis();
                        long dur = after - before;
                        if (dur == 0) {
                            dur = 1;
                        }
                        sumTime -= times[timeIndex];
                        sumTime += dur;
                        times[timeIndex] = dur;
                        timeIndex = (timeIndex + 1) % NUM_TIMES;

                        if (t2 == Float.POSITIVE_INFINITY) {
                            waitTime = Long.MAX_VALUE;
                        } else {
                            waitTime = before + (long) (t2 * 1000) - 1000;
                            if (waitTime < after) {
                                waitTime = after;
                            }
                            if (animationLimitingMode != 0) {
                                float ave = (float) sumTime / NUM_TIMES;
                                float delay;
                                if (animationLimitingMode == 1) {
                                    // %cpu
                                    delay = ave / animationLimitingAmount - ave;
                                } else {
                                    // fps
                                    delay = 1000f / animationLimitingAmount - ave;
                                }
                                long newWaitTime = after + (long) delay;
                                if (newWaitTime > waitTime) {
                                    waitTime = newWaitTime;
                                }
                            }
                        }
//                         frames++;
                    } catch (AnimationException ex) {
                        throw new BridgeException
                            (eng.ctx, ex.getElement().getElement(),
                             ex.getMessage());
                    }
                    exceptionCount = 0;
                } catch (Exception ex) {
                    if (++exceptionCount < MAX_EXCEPTION_COUNT) {
                        if (eng.ctx.getUserAgent() == null) {
                            ex.printStackTrace();
                        } else {
                            eng.ctx.getUserAgent().displayError(ex);
                        }
                    }
                }

                if (animationLimitingMode == 0) {
                    // so we don't steal too much time from the Swing thread
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException ie) {
                    }
                }
            }
        }

        /**
         * Returns the SVGAnimationEngine this AnimationTickRunnable is for.
         */
        protected SVGAnimationEngine getAnimationEngine() {
            return (SVGAnimationEngine) engRef.get();
        }
    }

    /**
     * The thread that ticks the animation.
     */
    protected class AnimationThread extends Thread {

        /**
         * The current time.
         */
        protected Calendar time = Calendar.getInstance();

        /**
         * The RunnableQueue to perform the animation in.
         */
        protected RunnableQueue runnableQueue =
            ctx.getUpdateManager().getUpdateRunnableQueue();

        /**
         * The animation ticker Runnable.
         */
        protected Ticker ticker = new Ticker();

        /**
         * Ticks the animation over as fast as possible.
         */
        public void run() {
            if (true) {
                for (;;) {
                    time.setTime(new Date());
                    ticker.t = timedDocumentRoot.convertWallclockTime(time);
                    try {
                        runnableQueue.invokeAndWait(ticker);
                    } catch (InterruptedException e) {
                        return;
                    }
                }
            } else {
                ticker.t = 1;
                while (ticker.t < 10) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                    }
                    try {
                        runnableQueue.invokeAndWait(ticker);
                    } catch (InterruptedException e) {
                        return;
                    }
                    ticker.t++;
                }
            }
        }

        /**
         * A runnable that ticks the animation engine.
         */
        protected class Ticker implements Runnable {

            /**
             * The document time to tick at next.
             */
            protected float t;

            /**
             * Ticks the animation over.
             */
            public void run() {
                tick(t, false);
            }
        }
    }

    // AnimatableValue factories

    /**
     * Interface for AnimatableValue factories.
     */
    protected interface Factory {

        /**
         * Creates a new AnimatableValue from a string.
         */
        AnimatableValue createValue(AnimationTarget target, String ns,
                                    String ln, boolean isCSS, String s);

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.
         */
        AnimatableValue createValue(AnimationTarget target, String pn, Value v);
    }

    /**
     * Factory class for AnimatableValues for CSS properties.
     * XXX Shorthand properties are not supported.
     */
    protected abstract class CSSValueFactory implements Factory {

        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            // XXX Always parsing as a CSS value.
            return createValue(target, ln, createCSSValue(target, ln, s));
        }

        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            CSSStylableElement elt = (CSSStylableElement) target.getElement();
            v = computeValue(elt, pn, v);
            return createAnimatableValue(target, pn, v);
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}, after
         * computation and inheritance.
         */
        protected abstract AnimatableValue createAnimatableValue
            (AnimationTarget target, String pn, Value v);

        /**
         * Creates a new CSS {@link Value} from a string.
         */
        protected Value createCSSValue(AnimationTarget t, String pn, String s) {
            CSSStylableElement elt = (CSSStylableElement) t.getElement();
            Value v = cssEngine.parsePropertyValue(elt, pn, s);
            return computeValue(elt, pn, v);
        }

        /**
         * Computes a CSS {@link Value} and performance inheritance if the
         * specified value is 'inherit'.
         */
        protected Value computeValue(CSSStylableElement elt, String pn,
                                     Value v) {
            ValueManager[] vms = cssEngine.getValueManagers();
            int idx = cssEngine.getPropertyIndex(pn);
            if (idx != -1) {
                if (v.getCssValueType() == CSSValue.CSS_INHERIT) {
                    elt = CSSEngine.getParentCSSStylableElement(elt);
                    if (elt != null) {
                        return cssEngine.getComputedStyle(elt, null, idx);
                    }
                    return vms[idx].getDefaultValue();
                }
                v = vms[idx].computeValue(elt, null, cssEngine, idx,
                                          dummyStyleMap, v);
            }
            return v;
        }
    }

    /**
     * Factory class for {@link AnimatableBooleanValue}s.
     */
    protected static class AnimatableBooleanValueFactory implements Factory {

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            return new AnimatableBooleanValue(target, "true".equals(s));
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return new AnimatableBooleanValue(target,
                                              "true".equals(v.getCssText()));
        }
    }

    /**
     * Factory class for {@link AnimatableIntegerValue}s.
     */
    protected static class AnimatableIntegerValueFactory implements Factory {

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            return new AnimatableIntegerValue(target, Integer.parseInt(s));
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return new AnimatableIntegerValue(target,
                                              Math.round(v.getFloatValue()));
        }
    }

    /**
     * Factory class for {@link AnimatableNumberValue}s.
     */
    protected static class AnimatableNumberValueFactory implements Factory {

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            return new AnimatableNumberValue(target, Float.parseFloat(s));
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return new AnimatableNumberValue(target, v.getFloatValue());
        }
    }

    /**
     * Factory class for {@link AnimatableNumberOrPercentageValue}s.
     */
    protected static class AnimatableNumberOrPercentageValueFactory
            implements Factory {

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            float v;
            boolean pc;
            if (s.charAt(s.length() - 1) == '%') {
                v = Float.parseFloat(s.substring(0, s.length() - 1));
                pc = true;
            } else {
                v = Float.parseFloat(s);
                pc = false;
            }
            return new AnimatableNumberOrPercentageValue(target, v, pc);
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            switch (v.getPrimitiveType()) {
                case CSSPrimitiveValue.CSS_PERCENTAGE:
                    return new AnimatableNumberOrPercentageValue
                        (target, v.getFloatValue(), true);
                case CSSPrimitiveValue.CSS_NUMBER:
                    return new AnimatableNumberOrPercentageValue
                        (target, v.getFloatValue());
            }
            // XXX Do something better than returning null.
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatablePreserveAspectRatioValue}s.
     */
    protected static class AnimatablePreserveAspectRatioValueFactory implements Factory {

        /**
         * The parsed 'align' value.
         */
        protected short align;

        /**
         * The parsed 'meetOrSlice' value.
         */
        protected short meetOrSlice;

        /**
         * Parser for preserveAspectRatio values.
         */
        protected PreserveAspectRatioParser parser =
            new PreserveAspectRatioParser();

        /**
         * Handler for the preserveAspectRatio parser.
         */
        protected DefaultPreserveAspectRatioHandler handler =
            new DefaultPreserveAspectRatioHandler() {

            /**
             * Implements {@link
             * PreserveAspectRatioHandler#startPreserveAspectRatio()}.
             */
            public void startPreserveAspectRatio() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_UNKNOWN;
                meetOrSlice = SVGPreserveAspectRatio.SVG_MEETORSLICE_UNKNOWN;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#none()}.
             */
            public void none() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_NONE;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMaxYMax()}.
             */
            public void xMaxYMax() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMAX;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMaxYMid()}.
             */
            public void xMaxYMid() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMID;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMaxYMin()}.
             */
            public void xMaxYMin() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMAXYMIN;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMidYMax()}.
             */
            public void xMidYMax() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMAX;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMidYMid()}.
             */
            public void xMidYMid() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMID;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMidYMin()}.
             */
            public void xMidYMin() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMIDYMIN;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMinYMax()}.
             */
            public void xMinYMax() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMAX;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMinYMid()}.
             */
            public void xMinYMid() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMID;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#xMinYMin()}.
             */
            public void xMinYMin() throws ParseException {
                align = SVGPreserveAspectRatio.SVG_PRESERVEASPECTRATIO_XMINYMIN;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#meet()}.
             */
            public void meet() throws ParseException {
                meetOrSlice = SVGPreserveAspectRatio.SVG_MEETORSLICE_MEET;
            }

            /**
             * Implements {@link PreserveAspectRatioHandler#slice()}.
             */
            public void slice() throws ParseException {
                meetOrSlice = SVGPreserveAspectRatio.SVG_MEETORSLICE_SLICE;
            }
        };

        /**
         * Creates a new AnimatablePreserveAspectRatioValueFactory.
         */
        public AnimatablePreserveAspectRatioValueFactory() {
            parser.setPreserveAspectRatioHandler(handler);
        }

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            try {
                parser.parse(s);
                return new AnimatablePreserveAspectRatioValue(target, align,
                                                              meetOrSlice);
            } catch (ParseException e) {
                // XXX Do something better than returning null.
                return null;
            }
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.  Returns null
         * since preserveAspectRatio values aren't used in CSS values.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatableLengthValue}s.
     */
    protected static class AnimatableLengthValueFactory implements Factory {

        /**
         * The parsed length unit type.
         */
        protected short type;

        /**
         * The parsed length value.
         */
        protected float value;

        /**
         * Parser for lengths.
         */
        protected LengthParser parser = new LengthParser();

        /**
         * Handler for the length parser.
         */
        protected LengthHandler handler = new DefaultLengthHandler() {
            public void startLength() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_NUMBER;
            }
            public void lengthValue(float v) throws ParseException {
                value = v;
            }
            public void em() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_EMS;
            }
            public void ex() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_EXS;
            }
            public void in() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_IN;
            }
            public void cm() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_CM;
            }
            public void mm() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_MM;
            }
            public void pc() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_PC;
            }
            public void pt() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_PT;
            }
            public void px() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_PX;
            }
            public void percentage() throws ParseException {
                type = SVGLength.SVG_LENGTHTYPE_PERCENTAGE;
            }
            public void endLength() throws ParseException {
            }
        };

        /**
         * Creates a new AnimatableLengthValueFactory.
         */
        public AnimatableLengthValueFactory() {
            parser.setLengthHandler(handler);
        }

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            short pcInterp = target.getPercentageInterpretation(ns, ln, isCSS);
            try {
                parser.parse(s);
                return new AnimatableLengthValue
                    (target, type, value, pcInterp);
            } catch (ParseException e) {
                // XXX Do something better than returning null.
                return null;
            }
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return new AnimatableIntegerValue(target,
                                              Math.round(v.getFloatValue()));
        }
    }

    /**
     * Factory class for {@link AnimatableLengthListValue}s.
     */
    protected static class AnimatableLengthListValueFactory implements Factory {

        /**
         * Parser for length lists.
         */
        protected LengthListParser parser = new LengthListParser();

        /**
         * The producer class that accumulates the lengths.
         */
        protected LengthArrayProducer producer = new LengthArrayProducer();

        /**
         * Creates a new AnimatableLengthListValueFactory.
         */
        public AnimatableLengthListValueFactory() {
            parser.setLengthListHandler(producer);
        }

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            try {
                short pcInterp = target.getPercentageInterpretation
                    (ns, ln, isCSS);
                parser.parse(s);
                return new AnimatableLengthListValue
                    (target, producer.getLengthTypeArray(),
                     producer.getLengthValueArray(),
                     pcInterp);
            } catch (ParseException e) {
                // XXX Do something better than returning null.
                return null;
            }
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.  Returns null
         * since point lists aren't used in CSS values.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatableNumberListValue}s.
     */
    protected static class AnimatableNumberListValueFactory implements Factory {

        /**
         * Parser for number lists.
         */
        protected NumberListParser parser = new NumberListParser();

        /**
         * The producer class that accumulates the numbers.
         */
        protected FloatArrayProducer producer = new FloatArrayProducer();

        /**
         * Creates a new AnimatableNumberListValueFactory.
         */
        public AnimatableNumberListValueFactory() {
            parser.setNumberListHandler(producer);
        }

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            try {
                parser.parse(s);
                return new AnimatableNumberListValue(target,
                                                     producer.getFloatArray());
            } catch (ParseException e) {
                // XXX Do something better than returning null.
                return null;
            }
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.  Returns null
         * since number lists aren't used in CSS values.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatableNumberListValue}s.
     */
    protected static class AnimatableRectValueFactory implements Factory {

        /**
         * Parser for number lists.
         */
        protected NumberListParser parser = new NumberListParser();

        /**
         * The producer class that accumulates the numbers.
         */
        protected FloatArrayProducer producer = new FloatArrayProducer();

        /**
         * Creates a new AnimatableNumberListValueFactory.
         */
        public AnimatableRectValueFactory() {
            parser.setNumberListHandler(producer);
        }

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            try {
                parser.parse(s);
                float[] r = producer.getFloatArray();
                if (r.length != 4) {
                    // XXX Do something better than returning null.
                    return null;
                }
                return new AnimatableRectValue(target, r[0], r[1], r[2], r[3]);
            } catch (ParseException e) {
                // XXX Do something better than returning null.
                return null;
            }
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.  Returns null
         * since rects aren't used in CSS values.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatablePointListValue}s.
     */
    protected static class AnimatablePointListValueFactory implements Factory {

        /**
         * Parser for point lists.
         */
        protected PointsParser parser = new PointsParser();

        /**
         * The producer class that accumulates the points.
         */
        protected FloatArrayProducer producer = new FloatArrayProducer();

        /**
         * Creates a new AnimatablePointListValueFactory.
         */
        public AnimatablePointListValueFactory() {
            parser.setPointsHandler(producer);
        }

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            try {
                parser.parse(s);
                return new AnimatablePointListValue(target,
                                                    producer.getFloatArray());
            } catch (ParseException e) {
                // XXX Do something better than returning null.
                return null;
            }
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.  Returns null
         * since point lists aren't used in CSS values.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatablePathDataValue}s.
     */
    protected static class AnimatablePathDataFactory implements Factory {

        /**
         * Parser for path data.
         */
        protected PathParser parser = new PathParser();

        /**
         * The producer class that accumulates the path segments.
         */
        protected PathArrayProducer producer = new PathArrayProducer();

        /**
         * Creates a new AnimatablePathDataFactory.
         */
        public AnimatablePathDataFactory() {
            parser.setPathHandler(producer);
        }

        /**
         * Creates a new AnimatableValue from a string.
         */
        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            try {
                parser.parse(s);
                return new AnimatablePathDataValue
                    (target, producer.getPathCommands(),
                     producer.getPathParameters());
            } catch (ParseException e) {
                // XXX Do something better than returning null.
                return null;
            }
        }

        /**
         * Creates a new AnimatableValue from a CSS {@link Value}.  Returns null
         * since point lists aren't used in CSS values.
         */
        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatableStringValue}s.
     */
    protected static class UncomputedAnimatableStringValueFactory implements Factory {

        public AnimatableValue createValue(AnimationTarget target, String ns,
                                           String ln, boolean isCSS, String s) {
            return new AnimatableStringValue(target, s);
        }

        public AnimatableValue createValue(AnimationTarget target, String pn,
                                           Value v) {
            return new AnimatableStringValue(target, v.getCssText());
        }
    }

    /**
     * Factory class for {@link AnimatableLengthOrIdentValue}s.
     */
    protected class AnimatableLengthOrIdentFactory extends CSSValueFactory {

        protected AnimatableValue createAnimatableValue(AnimationTarget target,
                                                        String pn, Value v) {
            if (v instanceof StringValue) {
                return new AnimatableLengthOrIdentValue(target,
                                                        v.getStringValue());
            }
            short pcInterp = target.getPercentageInterpretation(null, pn, true);
            FloatValue fv = (FloatValue) v;
            return new AnimatableLengthOrIdentValue
                (target, fv.getPrimitiveType(), fv.getFloatValue(), pcInterp);
        }
    }

    /**
     * Factory class for {@link AnimatableNumberOrIdentValue}s.
     */
    protected class AnimatableNumberOrIdentFactory extends CSSValueFactory {

        /**
         * Whether numbers are actually numeric keywords, as with the
         * font-weight property.
         */
        protected boolean numericIdents;

        public AnimatableNumberOrIdentFactory(boolean numericIdents) {
            this.numericIdents = numericIdents;
        }

        protected AnimatableValue createAnimatableValue(AnimationTarget target,
                                                        String pn, Value v) {
            if (v instanceof StringValue) {
                return new AnimatableNumberOrIdentValue(target,
                                                        v.getStringValue());
            }
            FloatValue fv = (FloatValue) v;
            return new AnimatableNumberOrIdentValue(target, fv.getFloatValue(),
                                                    numericIdents);
        }
    }

    /**
     * Factory class for {@link AnimatableAngleValue}s.
     */
    protected class AnimatableAngleValueFactory extends CSSValueFactory {

        protected AnimatableValue createAnimatableValue(AnimationTarget target,
                                                        String pn, Value v) {
            FloatValue fv = (FloatValue) v;
            short unit;
            switch (fv.getPrimitiveType()) {
                case CSSPrimitiveValue.CSS_NUMBER:
                case CSSPrimitiveValue.CSS_DEG:
                    unit = SVGAngle.SVG_ANGLETYPE_DEG;
                    break;
                case CSSPrimitiveValue.CSS_RAD:
                    unit = SVGAngle.SVG_ANGLETYPE_RAD;
                    break;
                case CSSPrimitiveValue.CSS_GRAD:
                    unit = SVGAngle.SVG_ANGLETYPE_GRAD;
                    break;
                default:
                    // XXX Do something better than returning null.
                    return null;
            }
            return new AnimatableAngleValue(target, fv.getFloatValue(), unit);
        }
    }

    /**
     * Factory class for {@link AnimatableAngleOrIdentValue}s.
     */
    protected class AnimatableAngleOrIdentFactory extends CSSValueFactory {

        protected AnimatableValue createAnimatableValue(AnimationTarget target,
                                                        String pn, Value v) {
            if (v instanceof StringValue) {
                return new AnimatableAngleOrIdentValue(target,
                                                       v.getStringValue());
            }
            FloatValue fv = (FloatValue) v;
            short unit;
            switch (fv.getPrimitiveType()) {
                case CSSPrimitiveValue.CSS_NUMBER:
                case CSSPrimitiveValue.CSS_DEG:
                    unit = SVGAngle.SVG_ANGLETYPE_DEG;
                    break;
                case CSSPrimitiveValue.CSS_RAD:
                    unit = SVGAngle.SVG_ANGLETYPE_RAD;
                    break;
                case CSSPrimitiveValue.CSS_GRAD:
                    unit = SVGAngle.SVG_ANGLETYPE_GRAD;
                    break;
                default:
                    // XXX Do something better than returning null.
                    return null;
            }
            return new AnimatableAngleOrIdentValue(target, fv.getFloatValue(),
                                                   unit);
        }
    }

    /**
     * Factory class for {@link AnimatableColorValue}s.
     */
    protected class AnimatableColorValueFactory extends CSSValueFactory {

        protected AnimatableValue createAnimatableValue(AnimationTarget target,
                                                        String pn, Value v) {
            Paint p = PaintServer.convertPaint
                (target.getElement(), null, v, 1.0f, ctx);
            if (p instanceof Color) {
                Color c = (Color) p;
                return new AnimatableColorValue(target,
                                                c.getRed() / 255f,
                                                c.getGreen() / 255f,
                                                c.getBlue() / 255f);
            }
            // XXX Indicate that the parsed value wasn't a Color?
            return null;
        }
    }

    /**
     * Factory class for {@link AnimatablePaintValue}s.
     */
    protected class AnimatablePaintValueFactory extends CSSValueFactory {

        /**
         * Creates a new {@link AnimatablePaintValue} from a {@link Color}
         * object.
         */
        protected AnimatablePaintValue createColorPaintValue(AnimationTarget t,
                                                             Color c) {
            return AnimatablePaintValue.createColorPaintValue
                (t, c.getRed() / 255f, c.getGreen() / 255f, c.getBlue() / 255f);

        }

        protected AnimatableValue createAnimatableValue(AnimationTarget target,
                                                        String pn, Value v) {
            if (v.getCssValueType() == CSSValue.CSS_PRIMITIVE_VALUE) {
                switch (v.getPrimitiveType()) {
                    case CSSPrimitiveValue.CSS_IDENT:
                        return AnimatablePaintValue.createNonePaintValue(target);
                    case CSSPrimitiveValue.CSS_RGBCOLOR: {
                        Paint p = PaintServer.convertPaint
                            (target.getElement(), null, v, 1.0f, ctx);
                        return createColorPaintValue(target, (Color) p);
                    }
                    case CSSPrimitiveValue.CSS_URI:
                        return AnimatablePaintValue.createURIPaintValue
                            (target, v.getStringValue());
                }
            } else {
                Value v1 = v.item(0);
                switch (v1.getPrimitiveType()) {
                    case CSSPrimitiveValue.CSS_RGBCOLOR: {
                        Paint p = PaintServer.convertPaint
                            (target.getElement(), null, v, 1.0f, ctx);
                        return createColorPaintValue(target, (Color) p);
                    }
                    case CSSPrimitiveValue.CSS_URI: {
                        Value v2 = v.item(1);
                        switch (v2.getPrimitiveType()) {
                            case CSSPrimitiveValue.CSS_IDENT:
                                return AnimatablePaintValue.createURINonePaintValue
                                    (target, v1.getStringValue());
                            case CSSPrimitiveValue.CSS_RGBCOLOR: {
                                Paint p = PaintServer.convertPaint
                                    (target.getElement(), null, v.item(1), 1.0f, ctx);
                                return createColorPaintValue(target, (Color) p);
                            }
                        }
                    }
                }
            }
            // XXX Indicate that the specified Value wasn't a Color?
            return null;
        }
    }

    /**
     * Factory class for computed CSS {@link AnimatableStringValue}s.
     */
    protected class AnimatableStringValueFactory extends CSSValueFactory {

        protected AnimatableValue createAnimatableValue(AnimationTarget target,
                                                        String pn, Value v) {
            return new AnimatableStringValue(target, v.getCssText());
        }
    }
}
