/*
 * 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.
 */

/* $Id: IFParser.java 1897194 2022-01-19 10:43:17Z ssteiner $ */

package org.apache.fop.render.intermediate;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.sax.SAXResult;
import javax.xml.transform.sax.SAXTransformerFactory;

import org.w3c.dom.DOMImplementation;
import org.w3c.dom.Document;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.xmlgraphics.util.QName;

import org.apache.fop.accessibility.AccessibilityEventProducer;
import org.apache.fop.accessibility.StructureTreeElement;
import org.apache.fop.accessibility.StructureTreeEventHandler;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.fo.ElementMapping;
import org.apache.fop.fo.ElementMappingRegistry;
import org.apache.fop.fo.expr.PropertyException;
import org.apache.fop.fo.extensions.InternalElementMapping;
import org.apache.fop.render.intermediate.extensions.DocumentNavigationExtensionConstants;
import org.apache.fop.render.intermediate.extensions.DocumentNavigationHandler;
import org.apache.fop.render.intermediate.extensions.GoToXYAction;
import org.apache.fop.traits.BorderProps;
import org.apache.fop.traits.RuleStyle;
import org.apache.fop.util.ColorUtil;
import org.apache.fop.util.ContentHandlerFactory;
import org.apache.fop.util.ContentHandlerFactoryRegistry;
import org.apache.fop.util.DOMBuilderContentHandlerFactory;
import org.apache.fop.util.DefaultErrorListener;
import org.apache.fop.util.LanguageTags;
import org.apache.fop.util.XMLUtil;

/**
 * This is a parser for the intermediate format XML which converts the intermediate file into
 * {@link IFPainter} events.
 */
public class IFParser implements IFConstants {

    /** Logger instance */
    protected static final Log log = LogFactory.getLog(IFParser.class);

    private static SAXTransformerFactory tFactory
        = (SAXTransformerFactory)SAXTransformerFactory.newInstance();

    private static Set<String> handledNamespaces = new java.util.HashSet<String>();

    static {
        handledNamespaces.add(XMLNS_NAMESPACE_URI);
        handledNamespaces.add(XML_NAMESPACE);
        handledNamespaces.add(NAMESPACE);
        handledNamespaces.add(XLINK_NAMESPACE);
    }

    /**
     * Parses an intermediate file and paints it.
     * @param src the Source instance pointing to the intermediate file
     * @param documentHandler the intermediate format document handler used to process the IF events
     * @param userAgent the user agent
     * @throws TransformerException if an error occurs while parsing the area tree XML
     * @throws IFException if an IF-related error occurs inside the target document handler
     */
    public void parse(Source src, IFDocumentHandler documentHandler, FOUserAgent userAgent)
            throws TransformerException, IFException {
        try {
            Transformer transformer = tFactory.newTransformer();
            transformer.setErrorListener(new DefaultErrorListener(log));

            SAXResult res = new SAXResult(getContentHandler(documentHandler, userAgent));

            transformer.transform(src, res);
        } catch (TransformerException te) {
            Throwable cause = te.getCause();
            //Unpack original IFException if applicable
            if (cause instanceof SAXException) {
                SAXException se = (SAXException) cause;
                cause = se.getCause();
                if (cause instanceof IFException) {
                    throw (IFException) cause;
                }
            } else if (cause instanceof IFException) {
                throw (IFException) cause;
            }
            throw te;
        }
    }

    /**
     * Creates a new ContentHandler instance that you can send the area tree XML to. The parsed
     * pages are added to the AreaTreeModel instance you pass in as a parameter.
     * @param documentHandler the intermediate format document handler used to process the IF events
     * @param userAgent the user agent
     * @return the ContentHandler instance to receive the SAX stream from the area tree XML
     */
    public ContentHandler getContentHandler(IFDocumentHandler documentHandler,
                    FOUserAgent userAgent) {
        ElementMappingRegistry elementMappingRegistry
            = userAgent.getElementMappingRegistry();
        return new Handler(documentHandler, userAgent, elementMappingRegistry);
    }

    private static class Handler extends DefaultHandler {

        private Map<String, ElementHandler> elementHandlers = new HashMap<String, ElementHandler>();

        private IFDocumentHandler documentHandler;
        private IFPainter painter;
        private FOUserAgent userAgent;
        private ElementMappingRegistry elementMappingRegistry;

        private Attributes lastAttributes;

        private StringBuffer content = new StringBuffer();
        private boolean ignoreCharacters = true;

        //private Stack delegateStack = new Stack();
        private int delegateDepth;
        private ContentHandler delegate;
        private boolean inForeignObject;
        private Document foreignObject;

        private ContentHandler navParser;

        private StructureTreeHandler structureTreeHandler;

        private Attributes pageSequenceAttributes;

        private Map<String, StructureTreeElement> structureTreeElements
                = new HashMap<String, StructureTreeElement>();
        private Map<String, GoToXYAction> unresolvedIds = new HashMap<>();

        private class StructureTreeHandler extends DefaultHandler {

            protected final StructureTreeEventHandler structureTreeEventHandler;

            StructureTreeHandler(StructureTreeEventHandler structureTreeEventHandler) {
                this.structureTreeEventHandler = structureTreeEventHandler;
            }

            void startStructureTree(String type) {
            }

            @Override
            public void startElement(String uri, String localName, String qName,
                    Attributes attributes) throws SAXException {
                if (!"structure-tree".equals(localName)) {
                    if (localName.equals("marked-content")) {
                        localName = "#PCDATA";
                    }
                    StructureTreeElement parent = getStructureTreeElement(attributes);
                    String structID = attributes.getValue(InternalElementMapping.URI,
                            InternalElementMapping.STRUCT_ID);
                    if (structID == null) {
                        structureTreeEventHandler.startNode(localName, attributes, parent);
                    } else if (localName.equals("external-graphic")
                            || localName.equals("instream-foreign-object")) {
                        StructureTreeElement structureTreeElement
                                = structureTreeEventHandler.startImageNode(localName, attributes, parent);
                        structureTreeElements.put(structID, structureTreeElement);
                    } else {
                        StructureTreeElement structureTreeElement = structureTreeEventHandler
                                    .startReferencedNode(localName, attributes, parent);
                        structureTreeElements.put(structID, structureTreeElement);
                    }
                }
            }

            @Override
            public void endElement(String uri, String localName, String arqNameg2)
                    throws SAXException {
                if (!"structure-tree".equals(localName)) {
                    structureTreeEventHandler.endNode(localName);
                }
            }
        }

        private class MainStructureTreeHandler extends StructureTreeHandler {

            private final Locale pageSequenceLanguage;

            MainStructureTreeHandler(StructureTreeEventHandler structureTreeEventHandler,
                    Locale pageSequenceLanguage) throws SAXException {
                super(structureTreeEventHandler);
                this.pageSequenceLanguage = pageSequenceLanguage;
            }

            @Override
            void startStructureTree(String type) {
                structureTreeEventHandler.startPageSequence(pageSequenceLanguage, type);
            }

            public void endDocument() throws SAXException {
                startIFElement(EL_PAGE_SEQUENCE, pageSequenceAttributes);
                pageSequenceAttributes = null;
            }

        }

        public Handler(IFDocumentHandler documentHandler, FOUserAgent userAgent,
                ElementMappingRegistry elementMappingRegistry) {
            this.documentHandler = documentHandler;
            this.userAgent = userAgent;
            this.elementMappingRegistry = elementMappingRegistry;
            elementHandlers.put(EL_DOCUMENT, new DocumentHandler());
            elementHandlers.put(EL_HEADER, new DocumentHeaderHandler());
            elementHandlers.put(EL_LOCALE, new LocaleHandler());
            elementHandlers.put(EL_TRAILER, new DocumentTrailerHandler());
            elementHandlers.put(EL_PAGE_SEQUENCE, new PageSequenceHandler());
            elementHandlers.put(EL_PAGE, new PageHandler());
            elementHandlers.put(EL_PAGE_HEADER, new PageHeaderHandler());
            elementHandlers.put(EL_PAGE_CONTENT, new PageContentHandler());
            elementHandlers.put(EL_PAGE_TRAILER, new PageTrailerHandler());
            //Page content
            elementHandlers.put(EL_VIEWPORT, new ViewportHandler());
            elementHandlers.put(EL_GROUP, new GroupHandler());
            elementHandlers.put(EL_ID, new IDHandler());
            elementHandlers.put(EL_FONT, new FontHandler());
            elementHandlers.put(EL_TEXT, new TextHandler());
            elementHandlers.put(EL_CLIP_RECT, new ClipRectHandler());
            elementHandlers.put(EL_RECT, new RectHandler());
            elementHandlers.put(EL_LINE, new LineHandler());
            elementHandlers.put(EL_BORDER_RECT, new BorderRectHandler());
            elementHandlers.put(EL_IMAGE, new ImageHandler());
        }

        private void establishForeignAttributes(Map<QName, String> foreignAttributes) {
            documentHandler.getContext().setForeignAttributes(foreignAttributes);
        }

        private void resetForeignAttributes() {
            documentHandler.getContext().resetForeignAttributes();
        }

        /** {@inheritDoc} */
        public void startElement(String uri, String localName, String qName, Attributes attributes)
                    throws SAXException {
            if (delegate != null) {
                delegateDepth++;
                delegate.startElement(uri, localName, qName, attributes);
            } else {
                boolean handled = true;
                if (NAMESPACE.equals(uri)) {
                    if (localName.equals(EL_PAGE_SEQUENCE) && userAgent.isAccessibilityEnabled()) {
                        pageSequenceAttributes = new AttributesImpl(attributes);
                        Locale language = getLanguage(attributes);
                        structureTreeHandler = new MainStructureTreeHandler(
                                userAgent.getStructureTreeEventHandler(), language);

                    } else if (localName.equals(EL_STRUCTURE_TREE)) {
                        if (userAgent.isAccessibilityEnabled()) {
                            String type = attributes.getValue("type");
                            structureTreeHandler.startStructureTree(type);
                            delegate = structureTreeHandler;
                        } else {
                            /* Delegate to a handler that does nothing */
                            delegate = new DefaultHandler();
                        }
                        delegateDepth++;
                        delegate.startDocument();
                        delegate.startElement(uri, localName, qName, attributes);
                    } else {
                        if (pageSequenceAttributes != null) {
                            /*
                             * This means that no structure-element tag was
                             * found in the XML, otherwise a
                             * StructureTreeBuilderWrapper object would have
                             * been created, which would have reset the
                             * pageSequenceAttributes field.
                             */
                            AccessibilityEventProducer.Provider
                                    .get(userAgent.getEventBroadcaster())
                                    .noStructureTreeInXML(this);
                        }
                        handled = startIFElement(localName, attributes);

                        if (EL_ID.equals(localName) && documentHandler.getDocumentNavigationHandler() != null) {
                            if (this.navParser == null) {
                                this.navParser = new DocumentNavigationHandler(
                                        documentHandler.getDocumentNavigationHandler(),
                                        structureTreeElements, unresolvedIds);
                            }
                            delegate = this.navParser;
                            delegateDepth++;
                            delegate.startDocument();
                            delegate.startElement(uri, localName, qName, attributes);
                        }
                    }
                } else if (DocumentNavigationExtensionConstants.NAMESPACE.equals(uri)) {
                    if (this.navParser == null) {
                        this.navParser = new DocumentNavigationHandler(
                                this.documentHandler.getDocumentNavigationHandler(),
                                        structureTreeElements, unresolvedIds);
                    }
                    delegate = this.navParser;
                    delegateDepth++;
                    delegate.startDocument();
                    delegate.startElement(uri, localName, qName, attributes);
                } else {
                    ContentHandlerFactoryRegistry registry
                            = userAgent.getContentHandlerFactoryRegistry();
                    ContentHandlerFactory factory = registry.getFactory(uri);
                    if (factory == null) {
                        DOMImplementation domImplementation
                            = elementMappingRegistry.getDOMImplementationForNamespace(uri);
                        if (domImplementation == null) {
                            domImplementation = ElementMapping.getDefaultDOMImplementation();
                            /*
                            throw new SAXException("No DOMImplementation could be"
                                    + " identified to handle namespace: " + uri);
                                    */
                        }
                        factory = new DOMBuilderContentHandlerFactory(uri, domImplementation);
                    }
                    delegate = factory.createContentHandler();
                    delegateDepth++;
                    delegate.startDocument();
                    delegate.startElement(uri, localName, qName, attributes);
                }
                if (!handled) {
                    if (uri == null || uri.length() == 0) {
                        throw new SAXException("Unhandled element " + localName
                                + " in namespace: " + uri);
                    } else {
                        log.warn("Unhandled element " + localName
                                + " in namespace: " + uri);
                    }
                }
            }
        }

        private static Locale getLanguage(Attributes attributes) {
            String xmllang = attributes.getValue(XML_NAMESPACE, "lang");
            return (xmllang == null) ? null : LanguageTags.toLocale(xmllang);
        }

        private boolean startIFElement(String localName, Attributes attributes)
                throws SAXException {
            lastAttributes = new AttributesImpl(attributes);
            ElementHandler elementHandler = elementHandlers.get(localName);
            content.setLength(0);
            ignoreCharacters = true;
            if (elementHandler != null) {
                ignoreCharacters = elementHandler.ignoreCharacters();
                try {
                    elementHandler.startElement(attributes);
                } catch (IFException ife) {
                    handleIFException(ife);
                }
                return true;
            } else {
                return false;
            }
        }

        private void handleIFException(IFException ife) throws SAXException {
            Throwable cause = ife.getCause();
            if (cause instanceof SAXException) {
                //unwrap
                throw (SAXException) cause;
            } else {
                //wrap
                throw new SAXException(ife);
            }
        }


        /** {@inheritDoc} */
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (delegate != null) {
                delegate.endElement(uri, localName, qName);
                delegateDepth--;
                if (delegateDepth == 0) {
                    delegate.endDocument();
                    if (delegate instanceof ContentHandlerFactory.ObjectSource) {
                        Object obj = ((ContentHandlerFactory.ObjectSource)delegate).getObject();
                        if (inForeignObject) {
                            this.foreignObject = (Document)obj;
                        } else {
                            handleExternallyGeneratedObject(obj);
                        }
                    }
                    delegate = null; //Sub-document is processed, return to normal processing
                }
            } else {
                if (NAMESPACE.equals(uri)) {
                    ElementHandler elementHandler = elementHandlers.get(localName);
                    if (elementHandler != null) {
                        try {
                            if (elementHandler instanceof DocumentHandler) {
                                addUnresolvedIds();
                            }
                            elementHandler.endElement();
                        } catch (IFException ife) {
                            handleIFException(ife);
                        }
                        content.setLength(0);
                    }
                    ignoreCharacters = true;
                } else {
                    if (log.isTraceEnabled()) {
                        log.trace("Ignoring " + localName + " in namespace: " + uri);
                    }
                }
            }
        }

        private void addUnresolvedIds() throws IFException {
            for (GoToXYAction action : unresolvedIds.values()) {
                if (action != null) {
                    documentHandler.getDocumentNavigationHandler().addResolvedAction(action);
                }
            }
        }

        // ============== Element handlers for the intermediate format =============

        private interface ElementHandler {
            void startElement(Attributes attributes) throws IFException, SAXException;
            void endElement() throws IFException;
            boolean ignoreCharacters();
        }

        private abstract class AbstractElementHandler implements ElementHandler {

            public void startElement(Attributes attributes) throws IFException, SAXException {
                //nop
            }

            public void endElement() throws IFException {
                //nop
            }

            public boolean ignoreCharacters() {
                return true;
            }
        }

        private class DocumentHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                documentHandler.startDocument();
            }

            public void endElement() throws IFException {
                documentHandler.endDocument();
            }

        }

        private class DocumentHeaderHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                documentHandler.startDocumentHeader();
            }

            public void endElement() throws IFException {
                documentHandler.endDocumentHeader();
            }

        }

        private class LocaleHandler extends AbstractElementHandler {
            public void startElement(Attributes attributes) throws IFException {
                documentHandler.setDocumentLocale(getLanguage(attributes));
            }
        }

        private class DocumentTrailerHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                documentHandler.startDocumentTrailer();
            }

            public void endElement() throws IFException {
                documentHandler.endDocumentTrailer();
            }

        }

        private class PageSequenceHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                String id = attributes.getValue("id");
                Locale language = getLanguage(attributes);
                if (language != null) {
                    documentHandler.getContext().setLanguage(language);
                }
                Map<QName, String> foreignAttributes = getForeignAttributes(lastAttributes);
                establishForeignAttributes(foreignAttributes);
                documentHandler.startPageSequence(id);
                resetForeignAttributes();
            }

            public void endElement() throws IFException {
                documentHandler.endPageSequence();
                documentHandler.getContext().setLanguage(null);
            }

        }

        private class PageHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                int index = Integer.parseInt(attributes.getValue("index"));
                String name = attributes.getValue("name");
                String pageMasterName = attributes.getValue("page-master-name");
                int width = Integer.parseInt(attributes.getValue("width"));
                int height = Integer.parseInt(attributes.getValue("height"));
                Map<QName, String> foreignAttributes = getForeignAttributes(lastAttributes);
                establishForeignAttributes(foreignAttributes);
                documentHandler.startPage(index, name, pageMasterName,
                        new Dimension(width, height));
                documentHandler.getContext().setPageNumber(index + 1);
                resetForeignAttributes();
            }

            public void endElement() throws IFException {
                documentHandler.endPage();
            }

        }

        private class PageHeaderHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                documentHandler.startPageHeader();
                structureTreeHandler = new StructureTreeHandler(userAgent.getStructureTreeEventHandler());
            }

            public void endElement() throws IFException {
                documentHandler.endPageHeader();
            }

        }

        private class PageContentHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                painter = documentHandler.startPageContent();
            }

            public void endElement() throws IFException {
                painter = null;
                documentHandler.getContext().setID("");
                documentHandler.endPageContent();
            }

        }

        private class PageTrailerHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                documentHandler.startPageTrailer();
            }

            public void endElement() throws IFException {
                documentHandler.endPageTrailer();
            }

        }

        private class ViewportHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                String transform = attributes.getValue("transform");
                AffineTransform[] transforms
                    = AffineTransformArrayParser.createAffineTransform(transform);
                int width = Integer.parseInt(attributes.getValue("width"));
                int height = Integer.parseInt(attributes.getValue("height"));
                Rectangle clipRect = XMLUtil.getAttributeAsRectangle(attributes, "clip-rect");
                painter.startViewport(transforms, new Dimension(width, height), clipRect);
                documentHandler.getContext().setRegionType(attributes.getValue("region-type"));
            }

            public void endElement() throws IFException {
                painter.endViewport();
            }

        }

        private class GroupHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                String transform = attributes.getValue("transform");
                AffineTransform[] transforms
                    = AffineTransformArrayParser.createAffineTransform(transform);
                String layer = attributes.getValue("layer");
                painter.startGroup(transforms, layer);
            }

            public void endElement() throws IFException {
                painter.endGroup();
            }

        }

        private class IDHandler extends AbstractElementHandler {

            @Override
            public void startElement(Attributes attributes) throws IFException, SAXException {
                String id = attributes.getValue("name");
                documentHandler.getContext().setID(id);
            }

        }

        private class FontHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                String family = attributes.getValue("family");
                String style = attributes.getValue("style");
                Integer weight = XMLUtil.getAttributeAsInteger(attributes, "weight");
                String variant = attributes.getValue("variant");
                Integer size = XMLUtil.getAttributeAsInteger(attributes, "size");
                Color color;
                try {
                    color = getAttributeAsColor(attributes, "color");
                } catch (PropertyException pe) {
                    throw new IFException("Error parsing the color attribute", pe);
                }
                painter.setFont(family, style, weight, variant, size, color);
            }

        }

        private class TextHandler extends AbstractElementHandler {

            public void endElement() throws IFException {
                int x = Integer.parseInt(lastAttributes.getValue("x"));
                int y = Integer.parseInt(lastAttributes.getValue("y"));
                String s = lastAttributes.getValue("letter-spacing");
                int letterSpacing = (s != null ? Integer.parseInt(s) : 0);
                s = lastAttributes.getValue("word-spacing");
                int wordSpacing = (s != null ? Integer.parseInt(s) : 0);
                int[] dx = XMLUtil.getAttributeAsIntArray(lastAttributes, "dx");
                int[][] dp = XMLUtil.getAttributeAsPositionAdjustments(lastAttributes, "dp");
                // if only DX present, then convert DX to DP; otherwise use only DP,
                // effectively ignoring DX
                if ((dp == null) && (dx != null)) {
                    dp = IFUtil.convertDXToDP(dx);
                }
                establishStructureTreeElement(lastAttributes);
                boolean isHyphenated = Boolean.valueOf(lastAttributes.getValue("hyphenated"));
                if (isHyphenated) {
                    documentHandler.getContext().setHyphenated(isHyphenated);
                }
                boolean nextIsSpace = Boolean.valueOf(lastAttributes.getValue("next-is-space"));
                painter.drawText(x, y, letterSpacing, wordSpacing, dp, content.toString(), nextIsSpace);
                documentHandler.getContext().setHyphenated(false);
                resetStructureTreeElement();
            }

            public boolean ignoreCharacters() {
                return false;
            }

        }

        private class ClipRectHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                int x = Integer.parseInt(attributes.getValue("x"));
                int y = Integer.parseInt(attributes.getValue("y"));
                int width = Integer.parseInt(attributes.getValue("width"));
                int height = Integer.parseInt(attributes.getValue("height"));
                BorderProps[] borders = new BorderProps[4];
                for (int i = 0; i < 4; i++) {
                    String b = attributes.getValue(SIDES[i]);
                    if (b != null) {
                        borders[i] = BorderProps.valueOf(userAgent, b);
                    }
                }

                if (!(borders[0] == null && borders[1] == null
                        && borders[2] == null && borders[3] == null)) {
                    painter.clipBackground(new Rectangle(x, y, width, height),
                            borders[0], borders[1], borders[2], borders[3]);
                }
                painter.clipRect(new Rectangle(x, y, width, height));
            }

        }

        private class RectHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                int x = Integer.parseInt(attributes.getValue("x"));
                int y = Integer.parseInt(attributes.getValue("y"));
                int width = Integer.parseInt(attributes.getValue("width"));
                int height = Integer.parseInt(attributes.getValue("height"));
                Color fillColor;
                try {
                    fillColor = getAttributeAsColor(attributes, "fill");
                } catch (PropertyException pe) {
                    throw new IFException("Error parsing the fill attribute", pe);
                }

                Rectangle rectangularArea = new Rectangle(x, y, width, height);

                //TODO should rect be overloaded to include rounded corners
                // or should we introduce a new IF element?
                BorderProps[] borders = new BorderProps[4];
                for (int i = 0; i < 4; i++) {
                    String b = attributes.getValue(SIDES[i]);
                    if (b != null) {
                        borders[i] = BorderProps.valueOf(userAgent, b);
                    }
                }

                if (!(borders[0] == null && borders[1] == null
                        && borders[2] == null && borders[3] == null)) {
                    painter.clipBackground(rectangularArea,
                            borders[0], borders[1], borders[2], borders[3]);
                }

                painter.fillRect(rectangularArea , fillColor);
            }

        }

        private class LineHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                int x1 = Integer.parseInt(attributes.getValue("x1"));
                int y1 = Integer.parseInt(attributes.getValue("y1"));
                int x2 = Integer.parseInt(attributes.getValue("x2"));
                int y2 = Integer.parseInt(attributes.getValue("y2"));
                int width = Integer.parseInt(attributes.getValue("stroke-width"));
                Color color;
                try {
                    color = getAttributeAsColor(attributes, "color");
                } catch (PropertyException pe) {
                    throw new IFException("Error parsing the fill attribute", pe);
                }
                RuleStyle style = RuleStyle.valueOf(attributes.getValue("style"));
                painter.drawLine(new Point(x1, y1), new Point(x2, y2), width, color, style);
            }

        }

        private static final String[] SIDES = new String[] {"top", "bottom", "left", "right"};

        private class BorderRectHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                int x = Integer.parseInt(attributes.getValue("x"));
                int y = Integer.parseInt(attributes.getValue("y"));
                int width = Integer.parseInt(attributes.getValue("width"));
                int height = Integer.parseInt(attributes.getValue("height"));
                BorderProps[] borders = new BorderProps[4];
                for (int i = 0; i < 4; i++) {
                    String b = attributes.getValue(SIDES[i]);
                    if (b != null) {
                        borders[i] = BorderProps.valueOf(userAgent, b);
                    }
                }
                Color backgroundColor;

                try {
                    backgroundColor = getAttributeAsColor(attributes, "inner-background-color");
                } catch (PropertyException pe) {
                    throw new IFException("Error parsing the color attribute", pe);
                }

                painter.drawBorderRect(new Rectangle(x, y, width, height),
                        borders[0], borders[1], borders[2], borders[3], backgroundColor);
            }

        }

        private class ImageHandler extends AbstractElementHandler {

            public void startElement(Attributes attributes) throws IFException {
                inForeignObject = true;
            }

            public void endElement() throws IFException {
                int x = Integer.parseInt(lastAttributes.getValue("x"));
                int y = Integer.parseInt(lastAttributes.getValue("y"));
                int width = Integer.parseInt(lastAttributes.getValue("width"));
                int height = Integer.parseInt(lastAttributes.getValue("height"));
                Map<QName, String> foreignAttributes = getForeignAttributes(lastAttributes);
                establishForeignAttributes(foreignAttributes);
                establishStructureTreeElement(lastAttributes);
                if (foreignObject != null) {
                    painter.drawImage(foreignObject,
                            new Rectangle(x, y, width, height));
                    foreignObject = null;
                } else {
                    String uri = lastAttributes.getValue(
                            XLINK_HREF.getNamespaceURI(), XLINK_HREF.getLocalName());
                    if (uri == null) {
                        throw new IFException("xlink:href is missing on image", null);
                    }
                    painter.drawImage(uri, new Rectangle(x, y, width, height));
                }
                resetStructureTreeElement();
                resetForeignAttributes();
                inForeignObject = false;
            }

            public boolean ignoreCharacters() {
                return false;
            }
        }


        // ====================================================================

        /**
         * Handles objects created by "sub-parsers" that implement the ObjectSource interface.
         * An example of object handled here are ExtensionAttachments.
         * @param obj the Object to be handled.
         * @throws SAXException if an error occurs while handling the extension object
         */
        protected void handleExternallyGeneratedObject(Object obj) throws SAXException {
            try {
                documentHandler.handleExtensionObject(obj);
            } catch (IFException ife) {
                handleIFException(ife);
            }
        }

        private Color getAttributeAsColor(Attributes attributes, String name)
                    throws PropertyException {
            String s = attributes.getValue(name);
            if (s == null) {
                return null;
            } else {
                return ColorUtil.parseColorString(userAgent, s);
            }
        }

        private static Map<QName, String> getForeignAttributes(Attributes atts) {
            Map<QName, String> foreignAttributes = null;
            for (int i = 0, c = atts.getLength(); i < c; i++) {
                String ns = atts.getURI(i);
                if (ns.length() > 0) {
                    if (handledNamespaces.contains(ns)) {
                        continue;
                    }
                    if (foreignAttributes == null) {
                        foreignAttributes = new java.util.HashMap<QName, String>();
                    }
                    QName qname = new QName(ns, atts.getQName(i));
                    foreignAttributes.put(qname, atts.getValue(i));
                }
            }
            return foreignAttributes;
        }

        private void establishStructureTreeElement(Attributes attributes) {
            StructureTreeElement element = getStructureTreeElement(attributes);
            if (element != null) {
                documentHandler.getContext().setStructureTreeElement(element);
            }
        }

        private StructureTreeElement getStructureTreeElement(Attributes attributes) {
            String structRef = attributes.getValue(InternalElementMapping.URI, InternalElementMapping.STRUCT_REF);
            if (structRef != null && structRef.length() > 0) {
                assert structureTreeElements.containsKey(structRef);
                return structureTreeElements.get(structRef);
            } else {
                return null;
            }
        }

        private void resetStructureTreeElement() {
            documentHandler.getContext().resetStructureTreeElement();
        }

        /** {@inheritDoc} */
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (delegate != null) {
                delegate.characters(ch, start, length);
            } else if (!ignoreCharacters) {
                this.content.append(ch, start, length);
            }
        }
    }
}
