/*
 * 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: IFSerializer.java 1875656 2020-03-25 16:36:47Z ssteiner $ */

package org.apache.fop.render.intermediate;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;

import org.apache.xmlgraphics.image.loader.ImageManager;
import org.apache.xmlgraphics.image.loader.ImageSessionContext;
import org.apache.xmlgraphics.util.QName;
import org.apache.xmlgraphics.util.XMLizable;

import org.apache.fop.accessibility.StructureTreeEventHandler;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.fo.extensions.InternalElementMapping;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.render.DefaultRendererConfigurator;
import org.apache.fop.render.RendererEventProducer;
import org.apache.fop.render.RenderingContext;
import org.apache.fop.render.intermediate.IFRendererConfig.IFRendererConfigParser;
import org.apache.fop.render.intermediate.IFStructureTreeBuilder.IFStructureTreeElement;
import org.apache.fop.render.intermediate.extensions.AbstractAction;
import org.apache.fop.render.intermediate.extensions.Bookmark;
import org.apache.fop.render.intermediate.extensions.BookmarkTree;
import org.apache.fop.render.intermediate.extensions.DocumentNavigationExtensionConstants;
import org.apache.fop.render.intermediate.extensions.Link;
import org.apache.fop.render.intermediate.extensions.NamedDestination;
import org.apache.fop.traits.BorderProps;
import org.apache.fop.traits.RuleStyle;
import org.apache.fop.util.ColorUtil;
import org.apache.fop.util.DOM2SAX;
import org.apache.fop.util.LanguageTags;
import org.apache.fop.util.XMLConstants;
import org.apache.fop.util.XMLUtil;


/**
 * IFPainter implementation that serializes the intermediate format to XML.
 */
public class IFSerializer extends AbstractXMLWritingIFDocumentHandler
implements IFConstants, IFPainter, IFDocumentNavigationHandler {

    /**
     * Intermediate Format (IF) version, used to express an @version attribute
     * in the root element of the IF document, the initial value of which
     * is set to '2.0' to signify that something preceded it (but didn't
     * happen to be marked as such), and that this version is not necessarily
     * backwards compatible with the unmarked (&lt;2.0) version.
     */
    public static final String VERSION = "2.0";

    private IFDocumentHandler mimicHandler;
    private int pageSequenceIndex; // used for accessibility

    /** Holds the intermediate format state */
    private IFState state;

    private String currentID = "";

    private IFStructureTreeBuilder structureTreeBuilder;
    private int pageNumberEnded;

    public IFSerializer(IFContext context) {
        super(context);
    }

    /** {@inheritDoc} */
    @Override
    protected String getMainNamespace() {
        return NAMESPACE;
    }

    /** {@inheritDoc} */
    public boolean supportsPagesOutOfOrder() {
        return false;
        //Theoretically supported but disabled to improve performance when
        //rendering the IF to the final format later on
    }

    /** {@inheritDoc} */
    public String getMimeType() {
        return MIME_TYPE;
    }

    /** {@inheritDoc} */
    public IFDocumentHandlerConfigurator getConfigurator() {
        if (this.mimicHandler != null) {
            return getMimickedDocumentHandler().getConfigurator();
        } else {
            return new DefaultRendererConfigurator(getUserAgent(), new IFRendererConfigParser());
        }
    }

    /** {@inheritDoc} */
    @Override
    public IFDocumentNavigationHandler getDocumentNavigationHandler() {
        return this;
    }

    /**
     * Tells this serializer to mimic the given document handler (mostly applies to the font set
     * that is used during layout).
     * @param targetHandler the document handler to mimic
     */
    public void mimicDocumentHandler(IFDocumentHandler targetHandler) {
        this.mimicHandler = targetHandler;
    }

    /**
     * Returns the document handler that is being mimicked by this serializer.
     * @return the mimicked document handler or null if no such document handler has been set
     */
    public IFDocumentHandler getMimickedDocumentHandler() {
        return this.mimicHandler;
    }

    /** {@inheritDoc} */
    public FontInfo getFontInfo() {
        if (this.mimicHandler != null) {
            return this.mimicHandler.getFontInfo();
        } else {
            return null;
        }
    }

    /** {@inheritDoc} */
    public void setFontInfo(FontInfo fontInfo) {
        if (this.mimicHandler != null) {
            this.mimicHandler.setFontInfo(fontInfo);
        }
    }

    /** {@inheritDoc} */
    public void setDefaultFontInfo(FontInfo fontInfo) {
        if (this.mimicHandler != null) {
            this.mimicHandler.setDefaultFontInfo(fontInfo);
        }
    }

    @Override
    public StructureTreeEventHandler getStructureTreeEventHandler() {
        if (structureTreeBuilder == null) {
            structureTreeBuilder = new IFStructureTreeBuilder();
        }
        return structureTreeBuilder;
    }

    /** {@inheritDoc} */
    @Override
    public void startDocument() throws IFException {
        super.startDocument();
        try {
            handler.startDocument();
            handler.startPrefixMapping("", NAMESPACE);
            handler.startPrefixMapping(XLINK_PREFIX, XLINK_NAMESPACE);
            handler.startPrefixMapping(DocumentNavigationExtensionConstants.PREFIX,
                    DocumentNavigationExtensionConstants.NAMESPACE);
            handler.startPrefixMapping(InternalElementMapping.STANDARD_PREFIX,
                    InternalElementMapping.URI);
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "version", VERSION);
            handler.startElement(EL_DOCUMENT, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in startDocument()", e);
        }
    }

    @Override
    public void setDocumentLocale(Locale locale) {
        AttributesImpl atts  = new AttributesImpl();
        atts.addAttribute(XML_NAMESPACE, "lang", "xml:lang", XMLUtil.CDATA,
                LanguageTags.toLanguageTag(locale));
        try {
            handler.startElement(EL_LOCALE, atts);
            handler.endElement(EL_LOCALE);
        } catch (SAXException e) {
            throw new RuntimeException("Unable to create the " + EL_LOCALE + " element.", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void startDocumentHeader() throws IFException {
        try {
            handler.startElement(EL_HEADER);
        } catch (SAXException e) {
            throw new IFException("SAX error in startDocumentHeader()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void endDocumentHeader() throws IFException {
        try {
            handler.endElement(EL_HEADER);
        } catch (SAXException e) {
            throw new IFException("SAX error in startDocumentHeader()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void startDocumentTrailer() throws IFException {
        try {
            handler.startElement(EL_TRAILER);
        } catch (SAXException e) {
            throw new IFException("SAX error in startDocumentTrailer()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void endDocumentTrailer() throws IFException {
        try {
            handler.endElement(EL_TRAILER);
        } catch (SAXException e) {
            throw new IFException("SAX error in endDocumentTrailer()", e);
        }
    }

    /** {@inheritDoc} */
    public void endDocument() throws IFException {
        try {
            handler.endElement(EL_DOCUMENT);
            handler.endDocument();
            finishDocumentNavigation();
        } catch (SAXException e) {
            throw new IFException("SAX error in endDocument()", e);
        }
    }

    /** {@inheritDoc} */
    public void startPageSequence(String id) throws IFException {
        try {
            AttributesImpl atts = new AttributesImpl();
            if (id != null) {
                atts.addAttribute(XML_NAMESPACE, "id", "xml:id", XMLUtil.CDATA, id);
            }
            Locale lang = getContext().getLanguage();
            if (lang != null) {
                atts.addAttribute(XML_NAMESPACE, "lang", "xml:lang", XMLUtil.CDATA,
                        LanguageTags.toLanguageTag(lang));
            }
            XMLUtil.addAttribute(atts, XMLConstants.XML_SPACE, "preserve");
            addForeignAttributes(atts);
            handler.startElement(EL_PAGE_SEQUENCE, atts);
            if (this.getUserAgent().isAccessibilityEnabled()) {
                assert (structureTreeBuilder != null);
                structureTreeBuilder.replayEventsForPageSequence(handler, pageSequenceIndex++);
            }
        } catch (SAXException e) {
            throw new IFException("SAX error in startPageSequence()", e);
        }
    }

    /** {@inheritDoc} */
    public void endPageSequence() throws IFException {
        try {

            handler.endElement(EL_PAGE_SEQUENCE);
        } catch (SAXException e) {
            throw new IFException("SAX error in endPageSequence()", e);
        }
    }

    /** {@inheritDoc} */
    public void startPage(int index, String name, String pageMasterName, Dimension size)
            throws IFException {
        try {
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "index", Integer.toString(index));
            addAttribute(atts, "name", name);
            if (pageMasterName != null) {
                //fox:external-document doesn't have a page-master
                addAttribute(atts, "page-master-name", pageMasterName);
            }
            addAttribute(atts, "width", Integer.toString(size.width));
            addAttribute(atts, "height", Integer.toString(size.height));
            addForeignAttributes(atts);
            getContext().setPageIndex(index);
            handler.startElement(EL_PAGE, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in startPage()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void startPageHeader() throws IFException {
        try {
            handler.startElement(EL_PAGE_HEADER);
            if (this.getUserAgent().isAccessibilityEnabled()) {
                structureTreeBuilder.replayEventsForRetrievedMarkers(handler);
            }
        } catch (SAXException e) {
            throw new IFException("SAX error in startPageHeader()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void endPageHeader() throws IFException {
        try {
            handler.endElement(EL_PAGE_HEADER);
        } catch (SAXException e) {
            throw new IFException("SAX error in endPageHeader()", e);
        }
    }

    /** {@inheritDoc} */
    public IFPainter startPageContent() throws IFException {
        try {
            handler.startElement(EL_PAGE_CONTENT);
            this.state = IFState.create();
            return this;
        } catch (SAXException e) {
            throw new IFException("SAX error in startPageContent()", e);
        }
    }

    /** {@inheritDoc} */
    public void endPageContent() throws IFException {
        try {
            this.state = null;
            currentID = "";
            handler.endElement(EL_PAGE_CONTENT);
        } catch (SAXException e) {
            throw new IFException("SAX error in endPageContent()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void startPageTrailer() throws IFException {
        try {
            handler.startElement(EL_PAGE_TRAILER);
        } catch (SAXException e) {
            throw new IFException("SAX error in startPageTrailer()", e);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void endPageTrailer() throws IFException {
        try {
            commitNavigation();
            handler.endElement(EL_PAGE_TRAILER);
        } catch (SAXException e) {
            throw new IFException("SAX error in endPageTrailer()", e);
        }
    }

    /** {@inheritDoc} */
    public void endPage() throws IFException {
        try {
            handler.endElement(EL_PAGE);
            getContext().setPageIndex(-1);
        } catch (SAXException e) {
            throw new IFException("SAX error in endPage()", e);
        }
        if (mimicHandler != null) {
            pageNumberEnded++;
            FOUserAgent userAgent = mimicHandler.getContext().getUserAgent();
            RendererEventProducer.Provider.get(userAgent.getEventBroadcaster()).endPage(this, pageNumberEnded);
        }
    }

    //---=== IFPainter ===---

    /** {@inheritDoc} */
    public void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect)
            throws IFException {
        startViewport(IFUtil.toString(transform), size, clipRect);
    }

    /** {@inheritDoc} */
    public void startViewport(AffineTransform[] transforms, Dimension size, Rectangle clipRect)
            throws IFException {
        startViewport(IFUtil.toString(transforms), size, clipRect);
    }

    private void startViewport(String transform, Dimension size, Rectangle clipRect)
            throws IFException {
        try {
            AttributesImpl atts = new AttributesImpl();
            if (transform != null && transform.length() > 0) {
                addAttribute(atts, "transform", transform);
            }
            addAttribute(atts, "width", Integer.toString(size.width));
            addAttribute(atts, "height", Integer.toString(size.height));
            if (clipRect != null) {
                addAttribute(atts, "clip-rect", IFUtil.toString(clipRect));
            }
            if (getUserAgent().isAccessibilityEnabled() && getContext().getRegionType() != null) {
                addAttribute(atts, "region-type", getContext().getRegionType());
            }
            handler.startElement(EL_VIEWPORT, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in startViewport()", e);
        }
    }

    /** {@inheritDoc} */
    public void endViewport() throws IFException {
        try {
            handler.endElement(EL_VIEWPORT);
        } catch (SAXException e) {
            throw new IFException("SAX error in endViewport()", e);
        }
    }

    /** {@inheritDoc} */
    public void startGroup(AffineTransform[] transforms, String layer) throws IFException {
        startGroup(IFUtil.toString(transforms), layer);
    }

    /** {@inheritDoc} */
    public void startGroup(AffineTransform transform, String layer) throws IFException {
        startGroup(IFUtil.toString(transform), layer);
    }

    private void startGroup(String transform, String layer) throws IFException {
        try {
            AttributesImpl atts = new AttributesImpl();
            if (transform != null && transform.length() > 0) {
                addAttribute(atts, "transform", transform);
            }
            if (layer != null && layer.length() > 0) {
                addAttribute(atts, "layer", layer);
            }
            handler.startElement(EL_GROUP, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in startGroup()", e);
        }
    }

    /** {@inheritDoc} */
    public void endGroup() throws IFException {
        try {
            handler.endElement(EL_GROUP);
        } catch (SAXException e) {
            throw new IFException("SAX error in endGroup()", e);
        }
    }

    /** {@inheritDoc} */
    public void drawImage(String uri, Rectangle rect) throws IFException {
        try {
            addID();
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, XLINK_HREF, uri);
            addAttribute(atts, "x", Integer.toString(rect.x));
            addAttribute(atts, "y", Integer.toString(rect.y));
            addAttribute(atts, "width", Integer.toString(rect.width));
            addAttribute(atts, "height", Integer.toString(rect.height));
            addForeignAttributes(atts);
            addStructureReference(atts);
            handler.element(EL_IMAGE, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in startGroup()", e);
        } finally {
            ImageSessionContext session = getUserAgent().getImageSessionContext();
            ImageManager imageManager = getUserAgent().getImageManager();
            imageManager.closeImage(uri, session);
        }
    }

    private void addForeignAttributes(AttributesImpl atts) throws SAXException {
        Map foreignAttributes = getContext().getForeignAttributes();
        if (!foreignAttributes.isEmpty()) {
            for (Object o : foreignAttributes.entrySet()) {
                Map.Entry entry = (Map.Entry) o;
                addAttribute(atts, (QName) entry.getKey(), entry.getValue().toString());
            }
        }
    }

    /** {@inheritDoc} */
    public void drawImage(Document doc, Rectangle rect) throws IFException {
        try {
            addID();
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "x", Integer.toString(rect.x));
            addAttribute(atts, "y", Integer.toString(rect.y));
            addAttribute(atts, "width", Integer.toString(rect.width));
            addAttribute(atts, "height", Integer.toString(rect.height));
            addForeignAttributes(atts);
            addStructureReference(atts);
            handler.startElement(EL_IMAGE, atts);
            new DOM2SAX(handler).writeDocument(doc, true);
            handler.endElement(EL_IMAGE);
        } catch (SAXException e) {
            throw new IFException("SAX error in startGroup()", e);
        }
    }

    private static String toString(Paint paint) {
        if (paint instanceof Color) {
            return ColorUtil.colorToString((Color)paint);
        } else {
            throw new UnsupportedOperationException("Paint not supported: " + paint);
        }
    }

    /** {@inheritDoc} */
    public void clipRect(Rectangle rect) throws IFException {
        try {
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "x", Integer.toString(rect.x));
            addAttribute(atts, "y", Integer.toString(rect.y));
            addAttribute(atts, "width", Integer.toString(rect.width));
            addAttribute(atts, "height", Integer.toString(rect.height));
            handler.element(EL_CLIP_RECT, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in clipRect()", e);
        }
    }

    /** {@inheritDoc} */
    public void clipBackground(Rectangle rect, BorderProps bpsBefore, BorderProps bpsAfter,
            BorderProps bpsStart, BorderProps bpsEnd) throws IFException {
        try {
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "x", Integer.toString(rect.x));
            addAttribute(atts, "y", Integer.toString(rect.y));
            addAttribute(atts, "width", Integer.toString(rect.width));
            addAttribute(atts, "height", Integer.toString(rect.height));
            if (hasRoundedCorners(bpsBefore, bpsAfter, bpsStart, bpsEnd)) {

                if (bpsBefore != null) {
                    addAttribute(atts, "top", bpsBefore.toString());
                }
                if (bpsAfter != null) {
                    addAttribute(atts, "bottom", bpsAfter.toString());
                }
                if (bpsStart != null) {
                    addAttribute(atts, "left", bpsStart.toString());
                }
                if (bpsEnd != null) {
                    addAttribute(atts, "right", bpsEnd.toString());
                }
            }
            handler.element(EL_CLIP_RECT, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in clipRect()", e);
        }
    }


    /** {@inheritDoc} */
    public void fillRect(Rectangle rect, Paint fill) throws IFException {
        if (fill == null) {
            return;
        }
        try {
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "x", Integer.toString(rect.x));
            addAttribute(atts, "y", Integer.toString(rect.y));
            addAttribute(atts, "width", Integer.toString(rect.width));
            addAttribute(atts, "height", Integer.toString(rect.height));
            addAttribute(atts, "fill", toString(fill));
            handler.element(EL_RECT, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in fillRect()", e);
        }
    }

    //TODO create a class representing all borders should exist
    //with query methods like this
    private boolean hasRoundedCorners(BorderProps bpsBefore, BorderProps bpsAfter,
            BorderProps bpsStart, BorderProps bpsEnd) {
        boolean rtn = false;

        if (bpsBefore != null && bpsBefore.getRadiusStart() > 0
                && bpsStart != null && bpsStart.getRadiusStart() > 0) {
            rtn = true;
        }

        if (bpsBefore != null && bpsBefore.getRadiusEnd() > 0
                && bpsEnd != null && bpsEnd.getRadiusStart() > 0) {
            rtn = true;
        }

        if (bpsEnd != null && bpsEnd.getRadiusEnd() > 0
                && bpsAfter != null && bpsAfter.getRadiusEnd() > 0) {
            rtn = true;
        }

        if (bpsAfter != null && bpsAfter.getRadiusStart() > 0
                && bpsStart != null && bpsStart.getRadiusEnd() > 0) {
            rtn = true;
        }

        return rtn;
    }

    /** {@inheritDoc} */
    public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom,
            BorderProps left, BorderProps right, Color innerBackgroundColor) throws IFException {
        if (top == null && bottom == null && left == null && right == null) {
            return;
        }
        try {
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "x", Integer.toString(rect.x));
            addAttribute(atts, "y", Integer.toString(rect.y));
            addAttribute(atts, "width", Integer.toString(rect.width));
            addAttribute(atts, "height", Integer.toString(rect.height));
            if (top != null) {
                addAttribute(atts, "top", top.toString());
            }
            if (bottom != null) {
                addAttribute(atts, "bottom", bottom.toString());
            }
            if (left != null) {
                addAttribute(atts, "left", left.toString());
            }
            if (right != null) {
                addAttribute(atts, "right", right.toString());
            }

            if (innerBackgroundColor != null) {
                addAttribute(atts, "inner-background-color",
                        ColorUtil.colorToString(innerBackgroundColor));
            }

            handler.element(EL_BORDER_RECT, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in drawBorderRect()", e);
        }
    }

    /** {@inheritDoc} */
    public void drawLine(Point start, Point end, int width, Color color, RuleStyle style)
            throws IFException {
        try {
            addID();
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "x1", Integer.toString(start.x));
            addAttribute(atts, "y1", Integer.toString(start.y));
            addAttribute(atts, "x2", Integer.toString(end.x));
            addAttribute(atts, "y2", Integer.toString(end.y));
            addAttribute(atts, "stroke-width", Integer.toString(width));
            addAttribute(atts, "color", ColorUtil.colorToString(color));
            addAttribute(atts, "style", style.getName());
            handler.element(EL_LINE, atts);
        } catch (SAXException e) {
            throw new IFException("SAX error in drawLine()", e);
        }
    }

    /** {@inheritDoc} */
    public void drawText(int x, int y, int letterSpacing, int wordSpacing,
                         int[][] dp, String text) throws IFException {
        drawText(x, y, letterSpacing, wordSpacing, dp, text, false);
    }

    /** {@inheritDoc} */
    public void drawText(int x, int y, int letterSpacing, int wordSpacing,
            int[][] dp, String text, boolean nextIsSpace) throws IFException {
        try {
            addID();
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "x", Integer.toString(x));
            addAttribute(atts, "y", Integer.toString(y));
            if (nextIsSpace) {
                addAttribute(atts, "next-is-space", "true");
            }
            if (letterSpacing != 0) {
                addAttribute(atts, "letter-spacing", Integer.toString(letterSpacing));
            }
            if (wordSpacing != 0) {
                addAttribute(atts, "word-spacing", Integer.toString(wordSpacing));
            }
            if (dp != null) {
                if (IFUtil.isDPIdentity(dp)) {
                    // don't add dx or dp attribute
                } else if (IFUtil.isDPOnlyDX(dp)) {
                    // add dx attribute only
                    int[] dx = IFUtil.convertDPToDX(dp);
                    addAttribute(atts, "dx", IFUtil.toString(dx));
                } else {
                    // add dp attribute only
                    addAttribute(atts, "dp", XMLUtil.encodePositionAdjustments(dp));
                }
            }
            addStructureReference(atts);
            if (getContext().isHyphenated()) {
                addAttribute(atts, "hyphenated", "true");
            }
            handler.startElement(EL_TEXT, atts);
            char[] chars = text.toCharArray();
            handler.characters(chars, 0, chars.length);
            handler.endElement(EL_TEXT);
        } catch (SAXException e) {
            throw new IFException("SAX error in setFont()", e);
        }
    }

    /** {@inheritDoc} */
    public void setFont(String family, String style, Integer weight, String variant, Integer size,
            Color color) throws IFException {
        try {
            AttributesImpl atts = new AttributesImpl();
            boolean changed;
            if (family != null) {
                changed = !family.equals(state.getFontFamily());
                if (changed) {
                    state.setFontFamily(family);
                    addAttribute(atts, "family", family);
                }
            }
            if (style != null) {
                changed = !style.equals(state.getFontStyle());
                if (changed) {
                    state.setFontStyle(style);
                    addAttribute(atts, "style", style);
                }
            }
            if (weight != null) {
                changed = (weight != state.getFontWeight());
                if (changed) {
                    state.setFontWeight(weight);
                    addAttribute(atts, "weight", weight.toString());
                }
            }
            if (variant != null) {
                changed = !variant.equals(state.getFontVariant());
                if (changed) {
                    state.setFontVariant(variant);
                    addAttribute(atts, "variant", variant);
                }
            }
            if (size != null) {
                changed = (size != state.getFontSize());
                if (changed) {
                    state.setFontSize(size);
                    addAttribute(atts, "size", size.toString());
                }
            }
            if (color != null) {
                changed = !org.apache.xmlgraphics.java2d.color.ColorUtil.isSameColor(
                        color, state.getTextColor());
                if (changed) {
                    state.setTextColor(color);
                    addAttribute(atts, "color", toString(color));
                }
            }
            if (atts.getLength() > 0) {
                handler.element(EL_FONT, atts);
            }
        } catch (SAXException e) {
            throw new IFException("SAX error in setFont()", e);
        }
    }

    /** {@inheritDoc} */
    public void handleExtensionObject(Object extension) throws IFException {
        if (extension instanceof XMLizable) {
            try {
                ((XMLizable)extension).toSAX(this.handler);
            } catch (SAXException e) {
                throw new IFException("SAX error while handling extension object", e);
            }
        } else {
            throw new UnsupportedOperationException(
                    "Extension must implement XMLizable: "
                            + extension + " (" + extension.getClass().getName() + ")");
        }
    }

    /**
     * @return a new rendering context
     * @throws IllegalStateException unless overridden
     */
    protected RenderingContext createRenderingContext() throws IllegalStateException {
        throw new IllegalStateException("Should never be called!");
    }

    private void addAttribute(AttributesImpl atts,
            org.apache.xmlgraphics.util.QName attribute, String value) throws SAXException {
        handler.startPrefixMapping(attribute.getPrefix(), attribute.getNamespaceURI());
        XMLUtil.addAttribute(atts, attribute, value);
    }

    private void addAttribute(AttributesImpl atts, String localName, String value) {
        XMLUtil.addAttribute(atts, localName, value);
    }

    private void addStructureReference(AttributesImpl atts) {
        IFStructureTreeElement structureTreeElement
                = (IFStructureTreeElement) getContext().getStructureTreeElement();
        if (structureTreeElement != null) {
            addStructRefAttribute(atts, structureTreeElement.getId());
        }
    }

    private void addStructRefAttribute(AttributesImpl atts, String id) {
        atts.addAttribute(InternalElementMapping.URI,
                InternalElementMapping.STRUCT_REF,
                InternalElementMapping.STANDARD_PREFIX + ":" + InternalElementMapping.STRUCT_REF,
                XMLConstants.CDATA,
                id);
    }

    private void addID() throws SAXException {
        String id = getContext().getID();
        if (!currentID.equals(id)) {
            AttributesImpl atts = new AttributesImpl();
            addAttribute(atts, "name", id);
            handler.startElement(EL_ID, atts);
            handler.endElement(EL_ID);
            currentID = id;
        }
    }

    private Map incompleteActions = new java.util.HashMap();
    private List completeActions = new java.util.LinkedList();

    private void noteAction(AbstractAction action) {
        if (action == null) {
            throw new NullPointerException("action must not be null");
        }
        if (!action.isComplete()) {
            assert action.hasID();
            incompleteActions.put(action.getID(), action);
        }
    }

    /** {@inheritDoc} */
    public void renderNamedDestination(NamedDestination destination) throws IFException {
        noteAction(destination.getAction());

        AttributesImpl atts = new AttributesImpl();
        atts.addAttribute("", "name", "name", XMLConstants.CDATA, destination.getName());
        try {
            handler.startElement(DocumentNavigationExtensionConstants.NAMED_DESTINATION, atts);
            serializeXMLizable(destination.getAction());
            handler.endElement(DocumentNavigationExtensionConstants.NAMED_DESTINATION);
        } catch (SAXException e) {
            throw new IFException("SAX error serializing named destination", e);
        }
    }

    /** {@inheritDoc} */
    public void renderBookmarkTree(BookmarkTree tree) throws IFException {
        AttributesImpl atts = new AttributesImpl();
        try {
            handler.startElement(DocumentNavigationExtensionConstants.BOOKMARK_TREE, atts);
            for (Object o : tree.getBookmarks()) {
                Bookmark b = (Bookmark) o;
                if (b.getAction() != null) {
                    serializeBookmark(b);
                }
            }
            handler.endElement(DocumentNavigationExtensionConstants.BOOKMARK_TREE);
        } catch (SAXException e) {
            throw new IFException("SAX error serializing bookmark tree", e);
        }
    }

    private void serializeBookmark(Bookmark bookmark) throws SAXException, IFException {
        noteAction(bookmark.getAction());

        AttributesImpl atts = new AttributesImpl();
        atts.addAttribute("", "title", "title", XMLUtil.CDATA, bookmark.getTitle());
        atts.addAttribute("", "starting-state", "starting-state",
                XMLUtil.CDATA, bookmark.isShown() ? "show" : "hide");
        handler.startElement(DocumentNavigationExtensionConstants.BOOKMARK, atts);
        serializeXMLizable(bookmark.getAction());
        for (Object o : bookmark.getChildBookmarks()) {
            Bookmark b = (Bookmark) o;
            if (b.getAction() != null) {
                serializeBookmark(b);
            }
        }
        handler.endElement(DocumentNavigationExtensionConstants.BOOKMARK);
    }

    /** {@inheritDoc} */
    public void renderLink(Link link) throws IFException {
        noteAction(link.getAction());

        AttributesImpl atts = new AttributesImpl();
        atts.addAttribute("", "rect", "rect",
                XMLConstants.CDATA, IFUtil.toString(link.getTargetRect()));
        if (getUserAgent().isAccessibilityEnabled()) {
            addStructRefAttribute(atts,
                    ((IFStructureTreeElement) link.getAction().getStructureTreeElement()).getId());
        }
        try {
            handler.startElement(DocumentNavigationExtensionConstants.LINK, atts);
            serializeXMLizable(link.getAction());
            handler.endElement(DocumentNavigationExtensionConstants.LINK);
        } catch (SAXException e) {
            throw new IFException("SAX error serializing link", e);
        }
    }

    /** {@inheritDoc} */
    public void addResolvedAction(AbstractAction action) throws IFException {
        assert action.isComplete();
        assert action.hasID();
        AbstractAction noted = (AbstractAction)incompleteActions.remove(action.getID());
        if (noted != null) {
            completeActions.add(action);
        } else {
            //ignore as it was already complete when it was first used.
        }
    }

    public int getPageIndex() {
        return -1;
    }

    private void commitNavigation() throws IFException {
        Iterator iter = this.completeActions.iterator();
        while (iter.hasNext()) {
            AbstractAction action = (AbstractAction)iter.next();
            iter.remove();
            serializeXMLizable(action);
        }
        assert this.completeActions.size() == 0;
    }

    private void finishDocumentNavigation() {
        assert this.incompleteActions.size() == 0 : "Still holding incomplete actions!";
    }

    private void serializeXMLizable(XMLizable object) throws IFException {
        try {
            object.toSAX(handler);
        } catch (SAXException e) {
            throw new IFException("SAX error serializing object", e);
        }
    }

    /** {@inheritDoc} */
    public boolean isBackgroundRequired(BorderProps bpsTop, BorderProps bpsBottom,
            BorderProps bpsLeft, BorderProps bpsRight) {
        return true;
    }
}
