/*

   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.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

import org.apache.batik.css.engine.CSSEngine;
import org.apache.batik.dom.util.XLinkSupport;
import org.apache.batik.dom.util.XMLSupport;
import org.apache.batik.gvt.GraphicsNode;
import org.apache.batik.parser.AWTTransformProducer;
import org.apache.batik.parser.ClockHandler;
import org.apache.batik.parser.ClockParser;
import org.apache.batik.parser.ParseException;
import org.apache.batik.util.ParsedURL;
import org.apache.batik.util.SVG12Constants;
import org.apache.batik.util.SVGConstants;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.svg.SVGDocument;
import org.w3c.dom.svg.SVGElement;
import org.w3c.dom.svg.SVGLangSpace;
import org.w3c.dom.svg.SVGNumberList;

/**
 * A collection of utility methods for SVG.
 *
 * @author <a href="mailto:tkormann@apache.org">Thierry Kormann</a>
 * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
 * @version $Id$
 */
public abstract class SVGUtilities implements SVGConstants, ErrorConstants {

    /**
     * No instance of this class is required.
     */
    protected SVGUtilities() {}

    ////////////////////////////////////////////////////////////////////////
    // common methods
    ////////////////////////////////////////////////////////////////////////

    /**
     * Returns the logical parent element of the given element.
     * The parent element of a used element is the &lt;use&gt; element
     * which reference it.
     */
    public static Element getParentElement(Element elt) {
        Node n = CSSEngine.getCSSParentNode(elt);
        while (n != null && n.getNodeType() != Node.ELEMENT_NODE) {
            n = CSSEngine.getCSSParentNode(n);
        }
        return (Element) n;
    }

    /**
     * Converts an SVGNumberList into a float array.
     * @param l the list to convert
     */
    public static float[] convertSVGNumberList(SVGNumberList l) {
        int n = l.getNumberOfItems();
        if (n == 0) {
            return null;
        }
        float[] fl = new float[n];
        for (int i=0; i < n; i++) {
            fl[i] = l.getItem(i).getValue();
        }
        return fl;
    }

    /**
     * Converts a string into a float.
     * @param s the float representation to convert
     */
    public static float convertSVGNumber(String s) {
        return Float.parseFloat(s);
    }

    /**
     * Converts a string into an integer.
     * @param s the integer representation to convert
     */
    public static int convertSVGInteger(String s) {
        return Integer.parseInt(s);
    }

    /**
     * Converts the specified ratio to float number.
     * @param v the ratio value to convert
     * @exception NumberFormatException if the ratio is not a valid
     * number or percentage
     */
    public static float convertRatio(String v) {
        float d = 1;
        if (v.endsWith("%")) {
            v = v.substring(0, v.length() - 1);
            d = 100;
        }
        float r = Float.parseFloat(v)/d;
        if (r < 0) {
            r = 0;
        } else if (r > 1) {
            r = 1;
        }
        return r;
    }

    /**
     * Returns the content of the 'desc' child of the given element.
     */
    public static String getDescription(SVGElement elt) {
        String result = "";
        boolean preserve = false;
        Node n = elt.getFirstChild();
        if (n != null && n.getNodeType() == Node.ELEMENT_NODE) {
            String name =
                (n.getPrefix() == null) ? n.getNodeName() : n.getLocalName();
            if (name.equals(SVG_DESC_TAG)) {
                preserve = ((SVGLangSpace)n).getXMLspace().equals
                    (SVG_PRESERVE_VALUE);
                for (n = n.getFirstChild();
                     n != null;
                     n = n.getNextSibling()) {
                    if (n.getNodeType() == Node.TEXT_NODE) {
                        result += n.getNodeValue();
                    }
                }
            }
        }
        return (preserve)
            ? XMLSupport.preserveXMLSpace(result)
            : XMLSupport.defaultXMLSpace(result);
    }

    /**
     * Tests whether or not the given element match a specified user agent.
     *
     * @param elt the element to check
     * @param ua the user agent
     */
    public static boolean matchUserAgent(Element elt, UserAgent ua) {
        test: if (elt.hasAttributeNS(null, SVG_SYSTEM_LANGUAGE_ATTRIBUTE)) {
            // Tests the system languages.
            String sl = elt.getAttributeNS(null,
                                           SVG_SYSTEM_LANGUAGE_ATTRIBUTE);
            if (sl.length() == 0) // SVG spec says empty returns false
                return false;
            StringTokenizer st = new StringTokenizer(sl, ", ");
            while (st.hasMoreTokens()) {
                String s = st.nextToken();
                if (matchUserLanguage(s, ua.getLanguages())) {
                    break test;
                }
            }
            return false;
        }
        if (elt.hasAttributeNS(null, SVG_REQUIRED_FEATURES_ATTRIBUTE)) {
            // Tests the system features.
            String rf = elt.getAttributeNS(null,
                                           SVG_REQUIRED_FEATURES_ATTRIBUTE);
            if (rf.length() == 0)  // SVG spec says empty returns false
                return false;
            StringTokenizer st = new StringTokenizer(rf, " ");
            while (st.hasMoreTokens()) {
                String s = st.nextToken();
                if (!ua.hasFeature(s)) {
                    return false;
                }
            }
        }
        if (elt.hasAttributeNS(null, SVG_REQUIRED_EXTENSIONS_ATTRIBUTE)) {
            // Tests the system features.
            String re = elt.getAttributeNS(null,
                                           SVG_REQUIRED_EXTENSIONS_ATTRIBUTE);
            if (re.length() == 0)  // SVG spec says empty returns false
                return false;
            StringTokenizer st = new StringTokenizer(re, " ");
            while (st.hasMoreTokens()) {
                String s = st.nextToken();
                if (!ua.supportExtension(s)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Tests whether or not the specified language specification matches
     * the user preferences.
     *
     * @param s the langage to check
     * @param userLanguages the user langages
     */
    protected static boolean matchUserLanguage(String s,
                                               String userLanguages) {
        StringTokenizer st = new StringTokenizer(userLanguages, ", ");
        while (st.hasMoreTokens()) {
            String t = st.nextToken();
            if (s.startsWith(t)) {
                if (s.length() > t.length()) {
                    return (s.charAt(t.length()) == '-');
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the value of the specified attribute specified on the
     * specified element or one of its ancestor. Ancestors are found
     * using the xlink:href attribute.
     *
     * @param element the element to start with
     * @param namespaceURI the namespace URI of the attribute to return
     * @param attrName the name of the attribute to search
     * @param ctx the bridge context
     * @return the value of the attribute or an empty string if not defined
     */
    public static String getChainableAttributeNS(Element element,
                                                 String namespaceURI,
                                                 String attrName,
                                                 BridgeContext ctx) {

        DocumentLoader loader = ctx.getDocumentLoader();
        Element e = element;
        List refs = new LinkedList();
        for (;;) {
            String v = e.getAttributeNS(namespaceURI, attrName);
            if (v.length() > 0) { // exit if attribute defined
                return v;
            }
            String uriStr = XLinkSupport.getXLinkHref(e);
            if (uriStr.length() == 0) { // exit if no more xlink:href
                return "";
            }
            String baseURI = e.getBaseURI();
            ParsedURL purl = new ParsedURL(baseURI, uriStr);

            for (Object ref : refs) {
                if (purl.equals(ref))
                    throw new BridgeException
                            (ctx, e, ERR_XLINK_HREF_CIRCULAR_DEPENDENCIES,
                                    new Object[]{uriStr});
            }

            try {
                SVGDocument svgDoc = (SVGDocument)e.getOwnerDocument();
                URIResolver resolver = ctx.createURIResolver(svgDoc, loader);
                e = resolver.getElement(purl.toString(), e);
                refs.add(purl);
            } catch(IOException ioEx ) {
                throw new BridgeException(ctx, e, ioEx, ERR_URI_IO,
                                          new Object[] {uriStr});
            } catch(SecurityException secEx ) {
                throw new BridgeException(ctx, e, secEx, ERR_URI_UNSECURE,
                                          new Object[] {uriStr});
            }
        }
    }

    /////////////////////////////////////////////////////////////////////////
    // <linearGradient> and <radialGradient>
    /////////////////////////////////////////////////////////////////////////

    /**
     * Returns a Point2D in user units according to the specified parameters.
     *
     * @param xStr the x coordinate
     * @param xAttr the name of the attribute that represents the x coordinate
     * @param yStr the y coordinate
     * @param yAttr the name of the attribute that represents the y coordinate
     * @param unitsType the coordinate system (OBJECT_BOUNDING_BOX |
     * USER_SPACE_ON_USE)
     * @param uctx the unit processor context
     */
    public static Point2D convertPoint(String xStr,
                                       String xAttr,
                                       String yStr,
                                       String yAttr,
                                       short unitsType,
                                       UnitProcessor.Context uctx) {
        float x, y;
        switch (unitsType) {
        case OBJECT_BOUNDING_BOX:
            x = UnitProcessor.svgHorizontalCoordinateToObjectBoundingBox
                (xStr, xAttr, uctx);
            y = UnitProcessor.svgVerticalCoordinateToObjectBoundingBox
                (yStr, yAttr, uctx);
            break;
        case USER_SPACE_ON_USE:
            x = UnitProcessor.svgHorizontalCoordinateToUserSpace
                (xStr, xAttr, uctx);
            y = UnitProcessor.svgVerticalCoordinateToUserSpace
                (yStr, yAttr, uctx);
            break;
        default:
            throw new IllegalArgumentException("Invalid unit type");
        }
        return new Point2D.Float(x, y);
    }

    /**
     * Returns a float in user units according to the specified parameters.
     *
     * @param length the length
     * @param attr the name of the attribute that represents the length
     * @param unitsType the coordinate system (OBJECT_BOUNDING_BOX |
     * USER_SPACE_ON_USE)
     * @param uctx the unit processor context
     */
    public static float convertLength(String length,
                                      String attr,
                                      short unitsType,
                                      UnitProcessor.Context uctx) {
        switch (unitsType) {
        case OBJECT_BOUNDING_BOX:
            return UnitProcessor.svgOtherLengthToObjectBoundingBox
                (length, attr, uctx);
        case USER_SPACE_ON_USE:
            return UnitProcessor.svgOtherLengthToUserSpace(length, attr, uctx);
        default:
            throw new IllegalArgumentException("Invalid unit type");
        }
    }

    /////////////////////////////////////////////////////////////////////////
    // <mask> region
    /////////////////////////////////////////////////////////////////////////

    /**
     * Returns the mask region according to the x, y, width, height,
     * and maskUnits attributes.
     *
     * @param maskElement the mask element that defines the various attributes
     * @param maskedElement the element referencing the mask
     * @param maskedNode the graphics node to mask (objectBoundingBox)
     * @param ctx the bridge context
     */
    public static Rectangle2D convertMaskRegion(Element maskElement,
                                                Element maskedElement,
                                                GraphicsNode maskedNode,
                                                BridgeContext ctx) {

        // 'x' attribute - default is -10%
        String xStr = maskElement.getAttributeNS(null, SVG_X_ATTRIBUTE);
        if (xStr.length() == 0) {
            xStr = SVG_MASK_X_DEFAULT_VALUE;
        }
        // 'y' attribute - default is -10%
        String yStr = maskElement.getAttributeNS(null, SVG_Y_ATTRIBUTE);
        if (yStr.length() == 0) {
            yStr = SVG_MASK_Y_DEFAULT_VALUE;
        }
        // 'width' attribute - default is 120%
        String wStr = maskElement.getAttributeNS(null, SVG_WIDTH_ATTRIBUTE);
        if (wStr.length() == 0) {
            wStr = SVG_MASK_WIDTH_DEFAULT_VALUE;
        }
        // 'height' attribute - default is 120%
        String hStr = maskElement.getAttributeNS(null, SVG_HEIGHT_ATTRIBUTE);
        if (hStr.length() == 0) {
            hStr = SVG_MASK_HEIGHT_DEFAULT_VALUE;
        }
        // 'maskUnits' attribute - default is 'objectBoundingBox'
        short unitsType;
        String units =
            maskElement.getAttributeNS(null, SVG_MASK_UNITS_ATTRIBUTE);
        if (units.length() == 0) {
            unitsType = OBJECT_BOUNDING_BOX;
        } else {
            unitsType = parseCoordinateSystem
                (maskElement, SVG_MASK_UNITS_ATTRIBUTE, units, ctx);
        }

        // resolve units in the (referenced) maskedElement's coordinate system
        UnitProcessor.Context uctx
            = UnitProcessor.createContext(ctx, maskedElement);

        return convertRegion(xStr,
                             yStr,
                             wStr,
                             hStr,
                             unitsType,
                             maskedNode,
                             uctx);
    }

    /////////////////////////////////////////////////////////////////////////
    // <pattern> region
    /////////////////////////////////////////////////////////////////////////

    /**
     * Returns the pattern region according to the x, y, width, height,
     * and patternUnits attributes.
     *
     * @param patternElement the pattern element that defines the attributes
     * @param paintedElement the element referencing the pattern
     * @param paintedNode the graphics node to paint (objectBoundingBox)
     * @param ctx the bridge context
     */
    public static Rectangle2D convertPatternRegion(Element patternElement,
                                                   Element paintedElement,
                                                   GraphicsNode paintedNode,
                                                   BridgeContext ctx) {

        // 'x' attribute - default is 0%
        String xStr = getChainableAttributeNS
            (patternElement, null, SVG_X_ATTRIBUTE, ctx);
        if (xStr.length() == 0) {
            xStr = SVG_PATTERN_X_DEFAULT_VALUE;
        }
        // 'y' attribute - default is 0%
        String yStr = getChainableAttributeNS
            (patternElement, null, SVG_Y_ATTRIBUTE, ctx);
        if (yStr.length() == 0) {
            yStr = SVG_PATTERN_Y_DEFAULT_VALUE;
        }
        // 'width' attribute - required
        String wStr = getChainableAttributeNS
            (patternElement, null, SVG_WIDTH_ATTRIBUTE, ctx);
        if (wStr.length() == 0) {
            throw new BridgeException
                (ctx, patternElement, ERR_ATTRIBUTE_MISSING,
                 new Object[] {SVG_WIDTH_ATTRIBUTE});
        }
        // 'height' attribute - required
        String hStr = getChainableAttributeNS
            (patternElement, null, SVG_HEIGHT_ATTRIBUTE, ctx);
        if (hStr.length() == 0) {
            throw new BridgeException
                (ctx, patternElement, ERR_ATTRIBUTE_MISSING,
                 new Object[] {SVG_HEIGHT_ATTRIBUTE});
        }
        // 'patternUnits' attribute - default is 'objectBoundingBox'
        short unitsType;
        String units = getChainableAttributeNS
            (patternElement, null, SVG_PATTERN_UNITS_ATTRIBUTE, ctx);
        if (units.length() == 0) {
            unitsType = OBJECT_BOUNDING_BOX;
        } else {
            unitsType = parseCoordinateSystem
                (patternElement, SVG_PATTERN_UNITS_ATTRIBUTE, units, ctx);
        }

        // resolve units in the (referenced) paintedElement's coordinate system
        UnitProcessor.Context uctx
            = UnitProcessor.createContext(ctx, paintedElement);

        return convertRegion(xStr,
                             yStr,
                             wStr,
                             hStr,
                             unitsType,
                             paintedNode,
                             uctx);
    }

    /////////////////////////////////////////////////////////////////////////
    // <filter> and filter primitive
    /////////////////////////////////////////////////////////////////////////

    /**
     * Returns an array of 2 float numbers that describes the filter
     * resolution of the specified filter element.
     *
     * @param filterElement the filter element
     * @param ctx the bridge context
     */
    public static
        float [] convertFilterRes(Element filterElement, BridgeContext ctx) {

        float [] filterRes = new float[2];
        String s = getChainableAttributeNS
            (filterElement, null, SVG_FILTER_RES_ATTRIBUTE, ctx);
        Float [] vals = convertSVGNumberOptionalNumber
            (filterElement, SVG_FILTER_RES_ATTRIBUTE, s, ctx);

        if (filterRes[0] < 0 || filterRes[1] < 0) {
            throw new BridgeException
                (ctx, filterElement, ERR_ATTRIBUTE_VALUE_MALFORMED,
                 new Object[] {SVG_FILTER_RES_ATTRIBUTE, s});
        }

        if (vals[0] == null)
            filterRes[0] = -1;
        else {
            filterRes[0] = vals[0];
            if (filterRes[0] < 0)
                throw new BridgeException
                    (ctx, filterElement, ERR_ATTRIBUTE_VALUE_MALFORMED,
                     new Object[] {SVG_FILTER_RES_ATTRIBUTE, s});
        }

        if (vals[1] == null)
            filterRes[1] = filterRes[0];
        else {
            filterRes[1] = vals[1];
            if (filterRes[1] < 0)
                throw new BridgeException
                    (ctx, filterElement, ERR_ATTRIBUTE_VALUE_MALFORMED,
                     new Object[] {SVG_FILTER_RES_ATTRIBUTE, s});
        }
        return filterRes;
    }

    /**
     * This function parses attrValue for a number followed by an optional
     * second Number. It always returns an array of two Floats.  If either
     * or both values are not provided the entries are set to null
     */
    public static Float[] convertSVGNumberOptionalNumber(Element elem,
                                                         String attrName,
                                                         String attrValue,
                                                         BridgeContext ctx) {

        Float[] ret = new Float[2];
        if (attrValue.length() == 0)
            return ret;

        try {
            StringTokenizer tokens = new StringTokenizer(attrValue, " ");
            ret[0] = Float.parseFloat(tokens.nextToken());
            if (tokens.hasMoreTokens()) {
                ret[1] = Float.parseFloat(tokens.nextToken());
            }

            if (tokens.hasMoreTokens()) {
                throw new BridgeException
                    (ctx, elem, ERR_ATTRIBUTE_VALUE_MALFORMED,
                     new Object[] {attrName, attrValue});
            }
        } catch (NumberFormatException nfEx ) {
            throw new BridgeException
                (ctx, elem, nfEx, ERR_ATTRIBUTE_VALUE_MALFORMED,
                 new Object[] {attrName, attrValue, nfEx });
        }
        return ret;
    }


   /**
    * Returns the filter region according to the x, y, width, height,
    * dx, dy, dw, dh and filterUnits attributes.
    *
    * @param filterElement the filter element that defines the attributes
    * @param filteredElement the element referencing the filter
    * @param filteredNode the graphics node to filter (objectBoundingBox)
    * @param ctx the bridge context
    */
   public static
       Rectangle2D convertFilterChainRegion(Element filterElement,
                                            Element filteredElement,
                                            GraphicsNode filteredNode,
                                            BridgeContext ctx) {

       // 'x' attribute - default is -10%
       String xStr = getChainableAttributeNS
           (filterElement, null, SVG_X_ATTRIBUTE, ctx);
       if (xStr.length() == 0) {
           xStr = SVG_FILTER_X_DEFAULT_VALUE;
       }
       // 'y' attribute - default is -10%
       String yStr = getChainableAttributeNS
           (filterElement, null, SVG_Y_ATTRIBUTE, ctx);
       if (yStr.length() == 0) {
           yStr = SVG_FILTER_Y_DEFAULT_VALUE;
       }
       // 'width' attribute - default is 120%
       String wStr = getChainableAttributeNS
           (filterElement, null, SVG_WIDTH_ATTRIBUTE, ctx);
       if (wStr.length() == 0) {
           wStr = SVG_FILTER_WIDTH_DEFAULT_VALUE;
       }
       // 'height' attribute - default is 120%
       String hStr = getChainableAttributeNS
           (filterElement, null, SVG_HEIGHT_ATTRIBUTE, ctx);
       if (hStr.length() == 0) {
           hStr = SVG_FILTER_HEIGHT_DEFAULT_VALUE;
       }
       // 'filterUnits' attribute - default is 'objectBoundingBox'
       short unitsType;
       String units = getChainableAttributeNS
           (filterElement, null, SVG_FILTER_UNITS_ATTRIBUTE, ctx);
       if (units.length() == 0) {
           unitsType = OBJECT_BOUNDING_BOX;
       } else {
           unitsType = parseCoordinateSystem
               (filterElement, SVG_FILTER_UNITS_ATTRIBUTE, units, ctx);
       }

       // The last paragraph of section 7.11 in SVG 1.1 states that objects
       // with zero width or height bounding boxes that use filters with
       // filterUnits="objectBoundingBox" must not use the filter.
       // TODO: Uncomment this after confirming this is the desired behaviour.
       /*AbstractGraphicsNodeBridge bridge =
           (AbstractGraphicsNodeBridge) ctx.getSVGContext(filteredElement);
       if (unitsType == OBJECT_BOUNDING_BOX && bridge != null) {
           Rectangle2D bbox = bridge.getBBox();
           if (bbox != null && bbox.getWidth() == 0 || bbox.getHeight() == 0) {
               return null;
           }
       }*/

       // resolve units in the (referenced) filteredElement's
       // coordinate system
       UnitProcessor.Context uctx
           = UnitProcessor.createContext(ctx, filteredElement);

       Rectangle2D region = convertRegion(xStr,
                                          yStr,
                                          wStr,
                                          hStr,
                                          unitsType,
                                          filteredNode,
                                          uctx);
       //
       // Account for region padding
       //
       units = getChainableAttributeNS
           (filterElement, null,
            SVG12Constants.SVG_FILTER_MARGINS_UNITS_ATTRIBUTE, ctx);
       if (units.length() == 0) {
           // Default to user space on use for margins, not objectBoundingBox
           unitsType = USER_SPACE_ON_USE;
       } else {
           unitsType = parseCoordinateSystem
               (filterElement,
                SVG12Constants.SVG_FILTER_MARGINS_UNITS_ATTRIBUTE, units, ctx);
       }

       // 'batik:dx' attribute - default is 0
       String dxStr = filterElement.getAttributeNS(null,
                                                   SVG12Constants.SVG_MX_ATRIBUTE);
       if (dxStr.length() == 0) {
           dxStr = SVG12Constants.SVG_FILTER_MX_DEFAULT_VALUE;
       }
       // 'batik:dy' attribute - default is 0
       String dyStr = filterElement.getAttributeNS(null, SVG12Constants.SVG_MY_ATRIBUTE);
       if (dyStr.length() == 0) {
           dyStr = SVG12Constants.SVG_FILTER_MY_DEFAULT_VALUE;
       }
       // 'batik:dw' attribute - default is 0
       String dwStr = filterElement.getAttributeNS(null, SVG12Constants.SVG_MW_ATRIBUTE);
       if (dwStr.length() == 0) {
           dwStr = SVG12Constants.SVG_FILTER_MW_DEFAULT_VALUE;
       }
       // 'batik:dh' attribute - default is 0
       String dhStr = filterElement.getAttributeNS(null, SVG12Constants.SVG_MH_ATRIBUTE);
       if (dhStr.length() == 0) {
           dhStr = SVG12Constants.SVG_FILTER_MH_DEFAULT_VALUE;
       }

       return extendRegion(dxStr,
                           dyStr,
                           dwStr,
                           dhStr,
                           unitsType,
                           filteredNode,
                           region,
                           uctx);
   }

   /**
    * Returns a rectangle that represents the region extended by the
    * specified differential coordinates.
    *
    * @param dxStr the differential x coordinate of the region
    * @param dyStr the differential y coordinate of the region
    * @param dwStr the differential width of the region
    * @param dhStr the differential height of the region
    * @param unitsType specifies whether the values are in userSpaceOnUse
    *        or objectBoundingBox space
    * @param region the region to extend
    * @param uctx the unit processor context (needed for userSpaceOnUse)
    */
    protected static Rectangle2D extendRegion(String dxStr,
                                              String dyStr,
                                              String dwStr,
                                              String dhStr,
                                              short unitsType,
                                              GraphicsNode filteredNode,
                                              Rectangle2D region,
                                              UnitProcessor.Context uctx) {

        float dx,dy,dw,dh;
        switch (unitsType) {
        case USER_SPACE_ON_USE:
            dx = UnitProcessor.svgHorizontalCoordinateToUserSpace
                (dxStr, SVG12Constants.SVG_MX_ATRIBUTE, uctx);
            dy = UnitProcessor.svgVerticalCoordinateToUserSpace
                (dyStr, SVG12Constants.SVG_MY_ATRIBUTE, uctx);
            dw = UnitProcessor.svgHorizontalCoordinateToUserSpace
                (dwStr, SVG12Constants.SVG_MW_ATRIBUTE, uctx);
            dh = UnitProcessor.svgVerticalCoordinateToUserSpace
                (dhStr, SVG12Constants.SVG_MH_ATRIBUTE, uctx);
            break;
        case OBJECT_BOUNDING_BOX:
            Rectangle2D bounds = filteredNode.getGeometryBounds();
            if (bounds == null) {
                dx = dy = dw = dh = 0;
            } else {
                dx = UnitProcessor.svgHorizontalCoordinateToObjectBoundingBox
                    (dxStr, SVG12Constants.SVG_MX_ATRIBUTE, uctx);
                dx *= bounds.getWidth();

                dy = UnitProcessor.svgVerticalCoordinateToObjectBoundingBox
                    (dyStr, SVG12Constants.SVG_MY_ATRIBUTE, uctx);
                dy *= bounds.getHeight();

                dw = UnitProcessor.svgHorizontalCoordinateToObjectBoundingBox
                    (dwStr, SVG12Constants.SVG_MW_ATRIBUTE, uctx);
                dw *= bounds.getWidth();

                dh = UnitProcessor.svgVerticalCoordinateToObjectBoundingBox
                    (dhStr, SVG12Constants.SVG_MH_ATRIBUTE, uctx);
                dh *= bounds.getHeight();
            }
            break;
        default:
            throw new IllegalArgumentException("Invalid unit type");
        }

        region.setRect(region.getX() + dx,
                       region.getY() + dy,
                       region.getWidth() + dw,
                       region.getHeight() + dh);

        return region;
    }


    public static Rectangle2D
        getBaseFilterPrimitiveRegion(Element filterPrimitiveElement,
                                     Element filteredElement,
                                     GraphicsNode filteredNode,
                                     Rectangle2D defaultRegion,
                                     BridgeContext ctx) {
        String s;

        // resolve units in the (referenced) filteredElement's
        // coordinate system
        UnitProcessor.Context uctx;
        uctx = UnitProcessor.createContext(ctx, filteredElement);

        // 'x' attribute - default is defaultRegion.getX()
        double x = defaultRegion.getX();
        s = filterPrimitiveElement.getAttributeNS(null, SVG_X_ATTRIBUTE);
        if (s.length() != 0) {
            x = UnitProcessor.svgHorizontalCoordinateToUserSpace
                (s, SVG_X_ATTRIBUTE, uctx);
        }

        // 'y' attribute - default is defaultRegion.getY()
        double y = defaultRegion.getY();
        s = filterPrimitiveElement.getAttributeNS(null, SVG_Y_ATTRIBUTE);
        if (s.length() != 0) {
            y = UnitProcessor.svgVerticalCoordinateToUserSpace
                (s, SVG_Y_ATTRIBUTE, uctx);
        }

        // 'width' attribute - default is defaultRegion.getWidth()
        double w = defaultRegion.getWidth();
        s = filterPrimitiveElement.getAttributeNS(null, SVG_WIDTH_ATTRIBUTE);
        if (s.length() != 0) {
            w = UnitProcessor.svgHorizontalLengthToUserSpace
                (s, SVG_WIDTH_ATTRIBUTE, uctx);
        }

        // 'height' attribute - default is defaultRegion.getHeight()
        double h = defaultRegion.getHeight();
        s = filterPrimitiveElement.getAttributeNS(null, SVG_HEIGHT_ATTRIBUTE);
        if (s.length() != 0) {
            h = UnitProcessor.svgVerticalLengthToUserSpace
                (s, SVG_HEIGHT_ATTRIBUTE, uctx);
        }

        // NOTE: it may be that dx/dy/dw/dh should be applied here
        //       but since this is mostly aimed at feImage I am
        //       unsure that it is really needed.
        return new Rectangle2D.Double(x, y, w, h);
    }

    /**
     * Returns the filter primitive region according to the x, y,
     * width, height, and filterUnits attributes. Processing the
     * element as the top one in the filter chain.
     *
     * @param filterPrimitiveElement the filter primitive element
     * @param filterElement the filter element
     * @param filteredElement the element referencing the filter
     * @param filteredNode the graphics node to use (objectBoundingBox)
     * @param defaultRegion the default region to filter
     * @param filterRegion the filter chain region
     * @param ctx the bridge context
     */
    public static Rectangle2D
        convertFilterPrimitiveRegion(Element filterPrimitiveElement,
                                     Element filterElement,
                                     Element filteredElement,
                                     GraphicsNode filteredNode,
                                     Rectangle2D defaultRegion,
                                     Rectangle2D filterRegion,
                                     BridgeContext ctx) {

        // 'primitiveUnits' - default is userSpaceOnUse
        String units = "";
        if (filterElement != null) {
            units = getChainableAttributeNS(filterElement,
                                            null,
                                            SVG_PRIMITIVE_UNITS_ATTRIBUTE,
                                            ctx);
        }
        short unitsType;
        if (units.length() == 0) {
            unitsType = USER_SPACE_ON_USE;
        } else {
            unitsType = parseCoordinateSystem
                (filterElement, SVG_FILTER_UNITS_ATTRIBUTE, units, ctx);
        }

        String xStr = "", yStr = "", wStr = "", hStr = "";

        if (filterPrimitiveElement != null) {
            // 'x' attribute - default is defaultRegion.getX()
            xStr = filterPrimitiveElement.getAttributeNS(null,
                                                         SVG_X_ATTRIBUTE);

            // 'y' attribute - default is defaultRegion.getY()
            yStr = filterPrimitiveElement.getAttributeNS(null,
                                                         SVG_Y_ATTRIBUTE);

            // 'width' attribute - default is defaultRegion.getWidth()
            wStr = filterPrimitiveElement.getAttributeNS(null,
                                                         SVG_WIDTH_ATTRIBUTE);

            // 'height' attribute - default is defaultRegion.getHeight()
            hStr = filterPrimitiveElement.getAttributeNS(null,
                                                         SVG_HEIGHT_ATTRIBUTE);
        }

        double x = defaultRegion.getX();
        double y = defaultRegion.getY();
        double w = defaultRegion.getWidth();
        double h = defaultRegion.getHeight();

        // resolve units in the (referenced) filteredElement's coordinate system
        UnitProcessor.Context uctx
            = UnitProcessor.createContext(ctx, filteredElement);

        switch (unitsType) {
        case OBJECT_BOUNDING_BOX:
            Rectangle2D bounds = filteredNode.getGeometryBounds();
            if (bounds != null) {
                if (xStr.length() != 0) {
                    x = UnitProcessor.svgHorizontalCoordinateToObjectBoundingBox
                        (xStr, SVG_X_ATTRIBUTE, uctx);
                    x = bounds.getX() + x*bounds.getWidth();
                }
                if (yStr.length() != 0) {
                    y = UnitProcessor.svgVerticalCoordinateToObjectBoundingBox
                        (yStr, SVG_Y_ATTRIBUTE, uctx);
                    y = bounds.getY() + y*bounds.getHeight();
                }
                if (wStr.length() != 0) {
                    w = UnitProcessor.svgHorizontalLengthToObjectBoundingBox
                        (wStr, SVG_WIDTH_ATTRIBUTE, uctx);
                    w *= bounds.getWidth();
                }
                if (hStr.length() != 0) {
                    h = UnitProcessor.svgVerticalLengthToObjectBoundingBox
                        (hStr, SVG_HEIGHT_ATTRIBUTE, uctx);
                    h *= bounds.getHeight();
                }
            }
            break;
        case USER_SPACE_ON_USE:
            if (xStr.length() != 0) {
                x = UnitProcessor.svgHorizontalCoordinateToUserSpace
                    (xStr, SVG_X_ATTRIBUTE, uctx);
            }
            if (yStr.length() != 0) {
                y = UnitProcessor.svgVerticalCoordinateToUserSpace
                    (yStr, SVG_Y_ATTRIBUTE, uctx);
            }
            if (wStr.length() != 0) {
                w = UnitProcessor.svgHorizontalLengthToUserSpace
                    (wStr, SVG_WIDTH_ATTRIBUTE, uctx);
            }
            if (hStr.length() != 0) {
                h = UnitProcessor.svgVerticalLengthToUserSpace
                    (hStr, SVG_HEIGHT_ATTRIBUTE, uctx);
            }
            break;
        default:
            throw new RuntimeException("invalid unitsType:" + unitsType); // can't be reached
        }

        Rectangle2D region = new Rectangle2D.Double(x, y, w, h);

        // Now, extend filter primitive region with dx/dy/dw/dh
        // settings (Batik extension). The dx/dy/dw/dh padding is
        // *always* in userSpaceOnUse space.

        units = "";
        if (filterElement != null) {
            units = getChainableAttributeNS
                (filterElement, null,
                 SVG12Constants.SVG_FILTER_PRIMITIVE_MARGINS_UNITS_ATTRIBUTE,
                 ctx);
        }

        if (units.length() == 0) {
            unitsType = USER_SPACE_ON_USE;
        } else {
            unitsType = parseCoordinateSystem
                (filterElement,
                 SVG12Constants.SVG_FILTER_PRIMITIVE_MARGINS_UNITS_ATTRIBUTE,
                 units, ctx);
        }

        String dxStr = "", dyStr = "", dwStr = "", dhStr = "";

        if (filterPrimitiveElement != null) {
            // 'batik:dx' attribute - default is 0
            dxStr = filterPrimitiveElement.getAttributeNS
                (null, SVG12Constants.SVG_MX_ATRIBUTE);

            // 'batik:dy' attribute - default is 0
            dyStr = filterPrimitiveElement.getAttributeNS
                (null, SVG12Constants.SVG_MY_ATRIBUTE);

            // 'batik:dw' attribute - default is 0
            dwStr = filterPrimitiveElement.getAttributeNS
                (null, SVG12Constants.SVG_MW_ATRIBUTE);

            // 'batik:dh' attribute - default is 0
            dhStr = filterPrimitiveElement.getAttributeNS
                (null, SVG12Constants.SVG_MH_ATRIBUTE);
        }
        if (dxStr.length() == 0) {
            dxStr = SVG12Constants.SVG_FILTER_MX_DEFAULT_VALUE;
        }
        if (dyStr.length() == 0) {
            dyStr = SVG12Constants.SVG_FILTER_MY_DEFAULT_VALUE;
        }
        if (dwStr.length() == 0) {
            dwStr = SVG12Constants.SVG_FILTER_MW_DEFAULT_VALUE;
        }
        if (dhStr.length() == 0) {
            dhStr = SVG12Constants.SVG_FILTER_MH_DEFAULT_VALUE;
        }

        region = extendRegion(dxStr,
                              dyStr,
                              dwStr,
                              dhStr,
                              unitsType,
                              filteredNode,
                              region,
                              uctx);

        Rectangle2D.intersect(region, filterRegion, region);

        return region;
    }

    /**
     * Returns the filter primitive region according to the x, y,
     * width, height, and filterUnits attributes. Processing the
     * element as the top one in the filter chain.
     *
     * @param filterPrimitiveElement the filter primitive element
     * @param filteredElement the element referencing the filter
     * @param filteredNode the graphics node to use (objectBoundingBox)
     * @param defaultRegion the default region to filter
     * @param filterRegion the filter chain region
     * @param ctx the bridge context
     */
    public static Rectangle2D
        convertFilterPrimitiveRegion(Element filterPrimitiveElement,
                                     Element filteredElement,
                                     GraphicsNode filteredNode,
                                     Rectangle2D defaultRegion,
                                     Rectangle2D filterRegion,
                                     BridgeContext ctx) {

        Node parentNode = filterPrimitiveElement.getParentNode();
        Element filterElement = null;
        if (parentNode != null &&
                parentNode.getNodeType() == Node.ELEMENT_NODE) {
            filterElement = (Element) parentNode;
        }
        return convertFilterPrimitiveRegion(filterPrimitiveElement,
                                            filterElement,
                                            filteredElement,
                                            filteredNode,
                                            defaultRegion,
                                            filterRegion,
                                            ctx);
    }

    /////////////////////////////////////////////////////////////////////////
    // region convenient methods
    /////////////////////////////////////////////////////////////////////////


    /** The userSpaceOnUse coordinate system constants. */
    public static final short USER_SPACE_ON_USE = 1;

    /** The objectBoundingBox coordinate system constants. */
    public static final short OBJECT_BOUNDING_BOX = 2;

    /** The strokeWidth coordinate system constants. */
    public static final short STROKE_WIDTH = 3;

    /**
     * Parses the specified coordinate system defined by the specified element.
     *
     * @param e the element that defines the coordinate system
     * @param attr the attribute which contains the coordinate system
     * @param coordinateSystem the coordinate system to parse
     * @param ctx the BridgeContext to use for error information
     * @return OBJECT_BOUNDING_BOX | USER_SPACE_ON_USE
     */
    public static short parseCoordinateSystem(Element e,
                                              String attr,
                                              String coordinateSystem,
                                              BridgeContext ctx) {
        if (SVG_USER_SPACE_ON_USE_VALUE.equals(coordinateSystem)) {
            return USER_SPACE_ON_USE;
        } else if (SVG_OBJECT_BOUNDING_BOX_VALUE.equals(coordinateSystem)) {
            return OBJECT_BOUNDING_BOX;
        } else {
            throw new BridgeException(ctx, e, ERR_ATTRIBUTE_VALUE_MALFORMED,
                                      new Object[] {attr, coordinateSystem});
        }
    }

    /**
     * Parses the specified coordinate system defined by the specified
     * marker element.
     *
     * @param e the element that defines the coordinate system
     * @param attr the attribute which contains the coordinate system
     * @param coordinateSystem the coordinate system to parse
     * @param ctx the BridgeContext to use for error information
     * @return STROKE_WIDTH | USER_SPACE_ON_USE
     */
    public static short parseMarkerCoordinateSystem(Element e,
                                                    String attr,
                                                    String coordinateSystem,
                                                    BridgeContext ctx) {
        if (SVG_USER_SPACE_ON_USE_VALUE.equals(coordinateSystem)) {
            return USER_SPACE_ON_USE;
        } else if (SVG_STROKE_WIDTH_VALUE.equals(coordinateSystem)) {
            return STROKE_WIDTH;
        } else {
            throw new BridgeException(ctx, e, ERR_ATTRIBUTE_VALUE_MALFORMED,
                                      new Object[] {attr, coordinateSystem});
        }
    }

    /**
     * Returns a rectangle that represents the region defined by the
     * specified coordinates.
     *
     * @param xStr the x coordinate of the region
     * @param yStr the y coordinate of the region
     * @param wStr the width of the region
     * @param hStr the height of the region
     * @param targetNode the graphics node (needed for objectBoundingBox)
     * @param uctx the unit processor context (needed for userSpaceOnUse)
     */
    protected static Rectangle2D convertRegion(String xStr,
                                               String yStr,
                                               String wStr,
                                               String hStr,
                                               short unitsType,
                                               GraphicsNode targetNode,
                                               UnitProcessor.Context uctx) {

        // construct the mask region in the appropriate coordinate system
        double x, y, w, h;
        switch (unitsType) {
        case OBJECT_BOUNDING_BOX:
            x = UnitProcessor.svgHorizontalCoordinateToObjectBoundingBox
                (xStr, SVG_X_ATTRIBUTE, uctx);
            y = UnitProcessor.svgVerticalCoordinateToObjectBoundingBox
                (yStr, SVG_Y_ATTRIBUTE, uctx);
            w = UnitProcessor.svgHorizontalLengthToObjectBoundingBox
                (wStr, SVG_WIDTH_ATTRIBUTE, uctx);
            h = UnitProcessor.svgVerticalLengthToObjectBoundingBox
                (hStr, SVG_HEIGHT_ATTRIBUTE, uctx);

            Rectangle2D bounds = targetNode.getGeometryBounds();
            if (bounds != null ) {
                x = bounds.getX() + x*bounds.getWidth();
                y = bounds.getY() + y*bounds.getHeight();
                w *= bounds.getWidth();
                h *= bounds.getHeight();
            } else {
                x = y = w = h = 0;
            }
            break;
        case USER_SPACE_ON_USE:
            x = UnitProcessor.svgHorizontalCoordinateToUserSpace
                (xStr, SVG_X_ATTRIBUTE, uctx);
            y = UnitProcessor.svgVerticalCoordinateToUserSpace
                (yStr, SVG_Y_ATTRIBUTE, uctx);
            w = UnitProcessor.svgHorizontalLengthToUserSpace
                (wStr, SVG_WIDTH_ATTRIBUTE, uctx);
            h = UnitProcessor.svgVerticalLengthToUserSpace
                (hStr, SVG_HEIGHT_ATTRIBUTE, uctx);
            break;
        default:
            throw new RuntimeException("invalid unitsType:" + unitsType ); // can't be reached
        }
        return new Rectangle2D.Double(x, y, w, h);
    }

    /////////////////////////////////////////////////////////////////////////
    // coordinate system and transformation support methods
    /////////////////////////////////////////////////////////////////////////

    /**
     * Returns an AffineTransform according to the specified parameters.
     *
     * @param e the element that defines the transform
     * @param attr the name of the attribute that represents the transform
     * @param transform the transform to parse
     * @param ctx the BridgeContext to use for error information
     */
    public static AffineTransform convertTransform(Element e,
                                                   String attr,
                                                   String transform,
                                                   BridgeContext ctx) {
        try {
            return AWTTransformProducer.createAffineTransform(transform);
        } catch (ParseException pEx) {
            throw new BridgeException(ctx, e, pEx, ERR_ATTRIBUTE_VALUE_MALFORMED,
                                      new Object[] {attr, transform, pEx });
        }
    }

    /**
     * Returns an AffineTransform to move to the objectBoundingBox
     * coordinate system.
     *
     * @param Tx the original transformation
     * @param node the graphics node that defines the coordinate
     *             system to move into
     */
    public static AffineTransform toObjectBBox(AffineTransform Tx,
                                               GraphicsNode node) {

        AffineTransform Mx = new AffineTransform();
        Rectangle2D bounds = node.getGeometryBounds();
        if (bounds != null) {
            Mx.translate(bounds.getX(), bounds.getY());
            Mx.scale(bounds.getWidth(), bounds.getHeight());
        }
        Mx.concatenate(Tx);
        return Mx;
    }

    /**
     * Returns the specified a Rectangle2D move to the objectBoundingBox
     * coordinate system of the specified graphics node.
     *
     * @param r the original Rectangle2D
     * @param node the graphics node that defines the coordinate
     *             system to move into
     */
    public static Rectangle2D toObjectBBox(Rectangle2D r,
                                           GraphicsNode node) {

        Rectangle2D bounds = node.getGeometryBounds();
        if (bounds != null) {
            return new Rectangle2D.Double
                (bounds.getX() + r.getX()*bounds.getWidth(),
                 bounds.getY() + r.getY()*bounds.getHeight(),
                 r.getWidth() * bounds.getWidth(),
                 r.getHeight() * bounds.getHeight());
        } else {
            return new Rectangle2D.Double();
        }
    }

    /**
     * Returns the value of the 'snapshotTime' attribute on the specified
     * element as a float, or <code>0f</code> if the attribute is missing
     * or given as <code>"none"</code>.
     *
     * @param e the element from which to retrieve the 'snapshotTime' attribute
     * @param ctx the BridgeContext to use for error information
     */
    public static float convertSnapshotTime(Element e, BridgeContext ctx) {
        if (!e.hasAttributeNS(null, SVG_SNAPSHOT_TIME_ATTRIBUTE)) {
            return 0f;
        }
        String t = e.getAttributeNS(null, SVG_SNAPSHOT_TIME_ATTRIBUTE);
        if (t.equals(SVG_NONE_VALUE)) {
            return 0f;
        }

        class Handler implements ClockHandler {
            float time;
            public void clockValue(float t) {
                time = t;
            }
        }
        ClockParser p = new ClockParser(false);
        Handler h = new Handler();
        p.setClockHandler(h);
        try {
            p.parse(t);
        } catch (ParseException pEx ) {
            throw new BridgeException
                (null, e, pEx, ERR_ATTRIBUTE_VALUE_MALFORMED,
                 new Object[] { SVG_SNAPSHOT_TIME_ATTRIBUTE, t, pEx });
        }
        return h.time;
    }
}
