/*
 * 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: RTFHandler.java 1785982 2017-03-08 15:15:06Z ssteiner $ */

package org.apache.fop.render.rtf;

// Java
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Iterator;
import java.util.Map;

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

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

import org.apache.xmlgraphics.image.loader.Image;
import org.apache.xmlgraphics.image.loader.ImageException;
import org.apache.xmlgraphics.image.loader.ImageFlavor;
import org.apache.xmlgraphics.image.loader.ImageInfo;
import org.apache.xmlgraphics.image.loader.ImageManager;
import org.apache.xmlgraphics.image.loader.ImageSessionContext;
import org.apache.xmlgraphics.image.loader.ImageSize;
import org.apache.xmlgraphics.image.loader.impl.ImageRawStream;
import org.apache.xmlgraphics.image.loader.impl.ImageXMLDOM;
import org.apache.xmlgraphics.image.loader.util.ImageUtil;

import org.apache.fop.ResourceEventProducer;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.datatypes.LengthBase;
import org.apache.fop.datatypes.PercentBaseContext;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.FOEventHandler;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.FOText;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.XMLObj;
import org.apache.fop.fo.flow.AbstractGraphics;
import org.apache.fop.fo.flow.BasicLink;
import org.apache.fop.fo.flow.Block;
import org.apache.fop.fo.flow.BlockContainer;
import org.apache.fop.fo.flow.Character;
import org.apache.fop.fo.flow.ExternalGraphic;
import org.apache.fop.fo.flow.Footnote;
import org.apache.fop.fo.flow.FootnoteBody;
import org.apache.fop.fo.flow.Inline;
import org.apache.fop.fo.flow.InstreamForeignObject;
import org.apache.fop.fo.flow.Leader;
import org.apache.fop.fo.flow.ListBlock;
import org.apache.fop.fo.flow.ListItem;
import org.apache.fop.fo.flow.ListItemBody;
import org.apache.fop.fo.flow.ListItemLabel;
import org.apache.fop.fo.flow.PageNumber;
import org.apache.fop.fo.flow.PageNumberCitation;
import org.apache.fop.fo.flow.PageNumberCitationLast;
import org.apache.fop.fo.flow.table.Table;
import org.apache.fop.fo.flow.table.TableBody;
import org.apache.fop.fo.flow.table.TableCell;
import org.apache.fop.fo.flow.table.TableColumn;
import org.apache.fop.fo.flow.table.TableFooter;
import org.apache.fop.fo.flow.table.TableHeader;
import org.apache.fop.fo.flow.table.TablePart;
import org.apache.fop.fo.flow.table.TableRow;
import org.apache.fop.fo.pagination.Flow;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fo.pagination.PageSequenceMaster;
import org.apache.fop.fo.pagination.Region;
import org.apache.fop.fo.pagination.SimplePageMaster;
import org.apache.fop.fo.pagination.StaticContent;
import org.apache.fop.fo.properties.CommonBorderPaddingBackground;
import org.apache.fop.fo.properties.EnumLength;
import org.apache.fop.fonts.FontSetup;
import org.apache.fop.layoutmgr.inline.ImageLayout;
import org.apache.fop.layoutmgr.table.ColumnSetup;
import org.apache.fop.render.RendererEventProducer;
import org.apache.fop.render.rtf.rtflib.exceptions.RtfException;
import org.apache.fop.render.rtf.rtflib.rtfdoc.IRtfAfterContainer;
import org.apache.fop.render.rtf.rtflib.rtfdoc.IRtfBeforeContainer;
import org.apache.fop.render.rtf.rtflib.rtfdoc.IRtfListContainer;
import org.apache.fop.render.rtf.rtflib.rtfdoc.IRtfTableContainer;
import org.apache.fop.render.rtf.rtflib.rtfdoc.IRtfTextrunContainer;
import org.apache.fop.render.rtf.rtflib.rtfdoc.ITableAttributes;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfAfter;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfAttributes;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfBefore;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfDocumentArea;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfElement;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfExternalGraphic;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfFile;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfFootnote;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfHyperLink;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfList;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfListItem;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfListItem.RtfListItemLabel;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfPage;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfParagraphBreak;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfSection;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfTable;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfTableCell;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfTableRow;
import org.apache.fop.render.rtf.rtflib.rtfdoc.RtfTextrun;
import org.apache.fop.render.rtf.rtflib.tools.BuilderContext;
import org.apache.fop.render.rtf.rtflib.tools.PercentContext;
import org.apache.fop.render.rtf.rtflib.tools.TableContext;

/**
 * RTF Handler: generates RTF output using the structure events from
 * the FO Tree sent to this structure handler.
 */
public class RTFHandler extends FOEventHandler {

    private RtfFile rtfFile;
    private final OutputStream os;
    private static Log log = LogFactory.getLog(RTFHandler.class);
    private RtfSection sect;
    private RtfDocumentArea docArea;
    private boolean bDefer;              //true, if each called handler shall be
                                         //processed at later time.
    private boolean bPrevHeaderSpecified; //true, if there has been a
                                                  //header in any page-sequence
    private boolean bPrevFooterSpecified; //true, if there has been a
                                                  //footer in any page-sequence
    private boolean bHeaderSpecified;  //true, if there is a header
                                               //in current page-sequence
    private boolean bFooterSpecified;  //true, if there is a footer
                                               //in current page-sequence
    private BuilderContext builderContext = new BuilderContext(null);

    private SimplePageMaster pagemaster;

    private int nestedTableDepth = 1;

    private PercentContext percentManager = new PercentContext();


    /**
     * Creates a new RTF structure handler.
     * @param userAgent the FOUserAgent for this process
     * @param os OutputStream to write to
     */
    public RTFHandler(FOUserAgent userAgent, OutputStream os) {
        super(userAgent);
        this.os = os;
        bDefer = true;

        boolean base14Kerning = false;
        FontSetup.setup(fontInfo, null, userAgent.getResourceResolver(), base14Kerning);
    }

    /**
     * Central exception handler for I/O exceptions.
     * @param ioe IOException to handle
     */
    protected void handleIOTrouble(IOException ioe) {
        RendererEventProducer eventProducer = RendererEventProducer.Provider.get(
                getUserAgent().getEventBroadcaster());
        eventProducer.ioError(this, ioe);
    }

    /** {@inheritDoc} */
    public void startDocument() throws SAXException {
        // TODO sections should be created
        try {
            rtfFile = new RtfFile(new OutputStreamWriter(os));
            docArea = rtfFile.startDocumentArea();
        } catch (IOException ioe) {
            // TODO could we throw Exception in all FOEventHandler events?
            throw new SAXException(ioe);
        }
    }

    /** {@inheritDoc} */
    public void endDocument() throws SAXException {
        try {
            rtfFile.flush();
        } catch (IOException ioe) {
            // TODO could we throw Exception in all FOEventHandler events?
            throw new SAXException(ioe);
        }
    }

    /** {@inheritDoc} */
    public void startPageSequence(PageSequence pageSeq)  {
        try {
            //This is needed for region handling
            if (this.pagemaster == null) {
                String reference = pageSeq.getMasterReference();
                this.pagemaster
                        = pageSeq.getRoot().getLayoutMasterSet().getSimplePageMaster(reference);
                if (this.pagemaster == null) {
                    RTFEventProducer eventProducer = RTFEventProducer.Provider.get(
                            getUserAgent().getEventBroadcaster());
                    eventProducer.onlySPMSupported(this, reference, pageSeq.getLocator());
                    PageSequenceMaster master
                        = pageSeq.getRoot().getLayoutMasterSet().getPageSequenceMaster(reference);
                    if (pageSeq.getMainFlow() != null) {
                        this.pagemaster = master.getNextSimplePageMaster(
                                false, false, false, false, pageSeq.getMainFlow().getFlowName());
                    }
                }
            }

            if (bDefer) {
                return;
            }

            sect = docArea.newSection();

            //read page size and margins, if specified
            //only simple-page-master supported, so pagemaster may be null
            if (pagemaster != null) {
                sect.getRtfAttributes().set(
                    PageAttributesConverter.convertPageAttributes(
                            pagemaster));
            } else {
                RTFEventProducer eventProducer = RTFEventProducer.Provider.get(
                        getUserAgent().getEventBroadcaster());
                eventProducer.noSPMFound(this, pageSeq.getLocator());
            }

            builderContext.pushContainer(sect);

            //Calculate usable page width for this flow
            int useAblePageWidth = pagemaster.getPageWidth().getValue()
                - pagemaster.getCommonMarginBlock().marginLeft.getValue()
                - pagemaster.getCommonMarginBlock().marginRight.getValue()
                - sect.getRtfAttributes().getValueAsInteger(RtfPage.MARGIN_LEFT)
                - sect.getRtfAttributes().getValueAsInteger(RtfPage.MARGIN_RIGHT);
            percentManager.setDimension(pageSeq, useAblePageWidth);

            bHeaderSpecified = false;
            bFooterSpecified = false;
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        }
    }

    /** {@inheritDoc} */
    public void endPageSequence(PageSequence pageSeq) {
        if (bDefer) {
            //If endBlock was called while SAX parsing, and the passed FO is Block
            //nested within another Block, stop deferring.
            //Now process all deferred FOs.
            bDefer = false;
            recurseFONode(pageSeq);
            this.pagemaster = null;
            bDefer = true;

            return;
        } else {
            builderContext.popContainer(RtfSection.class, this);
            this.pagemaster = null;
        }
    }

    /** {@inheritDoc} */
    public void startFlow(Flow fl) {
        if (bDefer) {
            return;
        }

        try {
            log.debug("starting flow: " + fl.getFlowName());
            boolean handled = false;
            Region regionBody = pagemaster.getRegion(Constants.FO_REGION_BODY);
            Region regionBefore = pagemaster.getRegion(Constants.FO_REGION_BEFORE);
            Region regionAfter = pagemaster.getRegion(Constants.FO_REGION_AFTER);
            if (fl.getFlowName().equals(regionBody.getRegionName())) {
                // if there is no header in current page-sequence but there has been
                // a header in a previous page-sequence, insert an empty header.
                if (bPrevHeaderSpecified && !bHeaderSpecified) {
                    RtfAttributes attr = new RtfAttributes();
                    attr.set(RtfBefore.HEADER);

                    final IRtfBeforeContainer contBefore
                        = (IRtfBeforeContainer)builderContext.getContainer(
                                IRtfBeforeContainer.class, true, this);
                    contBefore.newBefore(attr);
                }

                // if there is no footer in current page-sequence but there has been
                // a footer in a previous page-sequence, insert an empty footer.
                if (bPrevFooterSpecified && !bFooterSpecified) {
                    RtfAttributes attr = new RtfAttributes();
                    attr.set(RtfAfter.FOOTER);

                    final IRtfAfterContainer contAfter
                        = (IRtfAfterContainer)builderContext.getContainer(
                                IRtfAfterContainer.class, true, this);
                    contAfter.newAfter(attr);
                }
                handled = true;
            } else if (regionBefore != null
                    && fl.getFlowName().equals(regionBefore.getRegionName())) {
                bHeaderSpecified = true;
                bPrevHeaderSpecified = true;

                final IRtfBeforeContainer c
                    = (IRtfBeforeContainer)builderContext.getContainer(
                        IRtfBeforeContainer.class,
                        true, this);

                RtfAttributes beforeAttributes = ((RtfElement)c).getRtfAttributes();
                if (beforeAttributes == null) {
                    beforeAttributes = new RtfAttributes();
                }
                beforeAttributes.set(RtfBefore.HEADER);

                RtfBefore before = c.newBefore(beforeAttributes);
                builderContext.pushContainer(before);
                handled = true;
            } else if (regionAfter != null
                    && fl.getFlowName().equals(regionAfter.getRegionName())) {
                bFooterSpecified = true;
                bPrevFooterSpecified = true;

                final IRtfAfterContainer c
                    = (IRtfAfterContainer)builderContext.getContainer(
                        IRtfAfterContainer.class,
                        true, this);

                RtfAttributes afterAttributes = ((RtfElement)c).getRtfAttributes();
                if (afterAttributes == null) {
                    afterAttributes = new RtfAttributes();
                }

                afterAttributes.set(RtfAfter.FOOTER);

                RtfAfter after = c.newAfter(afterAttributes);
                builderContext.pushContainer(after);
                handled = true;
            }
            if (!handled) {
                log.warn("A " + fl.getLocalName() + " has been skipped: " + fl.getFlowName());
            }
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startFlow: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endFlow(Flow fl) {
        if (bDefer) {
            return;
        }

        try {
            Region regionBody = pagemaster.getRegion(Constants.FO_REGION_BODY);
            Region regionBefore = pagemaster.getRegion(Constants.FO_REGION_BEFORE);
            Region regionAfter = pagemaster.getRegion(Constants.FO_REGION_AFTER);
            if (fl.getFlowName().equals(regionBody.getRegionName())) {
                //just do nothing
            } else if (regionBefore != null
                    && fl.getFlowName().equals(regionBefore.getRegionName())) {
                builderContext.popContainer(RtfBefore.class, this);
            } else if (regionAfter != null
                    && fl.getFlowName().equals(regionAfter.getRegionName())) {
                builderContext.popContainer(RtfAfter.class, this);
            }
        } catch (Exception e) {
            log.error("endFlow: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void startBlock(Block bl) {
        if (bDefer) {
            return;
        }

        try {
            RtfAttributes rtfAttr
                = TextAttributesConverter.convertAttributes(bl);

            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class,
                    true, this);

            RtfTextrun textrun = container.getTextrun();

            textrun.addParagraphBreak();
            textrun.pushBlockAttributes(rtfAttr);
            textrun.addBookmark(bl.getId());
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startBlock: " + e.getMessage());
            throw new RuntimeException("Exception: " + e);
        }
    }

    /** {@inheritDoc} */
    public void endBlock(Block bl) {

        if (bDefer) {
            return;
        }

        try {
            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class,
                    true, this);

            RtfTextrun textrun = container.getTextrun();
            RtfParagraphBreak par = textrun.addParagraphBreak();

            RtfTableCell cellParent = (RtfTableCell)textrun.getParentOfClass(RtfTableCell.class);
            if (cellParent != null && par != null) {
                int iDepth = cellParent.findChildren(textrun);
                cellParent.setLastParagraph(par, iDepth);
            }

            int breakValue = toRtfBreakValue(bl.getBreakAfter());
            textrun.popBlockAttributes(breakValue);

        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startBlock:" + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void startBlockContainer(BlockContainer blc) {
        if (bDefer) {
            return;
        }

        try {
            RtfAttributes rtfAttr
                = TextAttributesConverter.convertBlockContainerAttributes(blc);

            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class,
                    true, this);

            RtfTextrun textrun = container.getTextrun();

            textrun.addParagraphBreak();
            textrun.pushBlockAttributes(rtfAttr);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startBlock: " + e.getMessage());
            throw new RuntimeException("Exception: " + e);
        }
    }

    /** {@inheritDoc} */
    public void endBlockContainer(BlockContainer bl) {
        if (bDefer) {
            return;
        }

        try {
            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class,
                    true, this);

            RtfTextrun textrun = container.getTextrun();

            textrun.addParagraphBreak();
            int breakValue = toRtfBreakValue(bl.getBreakAfter());
            textrun.popBlockAttributes(breakValue);

        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startBlock:" + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    private int toRtfBreakValue(int foBreakValue) {
        switch (foBreakValue) {
        case Constants.EN_PAGE:
            return RtfTextrun.BREAK_PAGE;
        case Constants.EN_EVEN_PAGE:
            return RtfTextrun.BREAK_EVEN_PAGE;
        case Constants.EN_ODD_PAGE:
            return RtfTextrun.BREAK_ODD_PAGE;
        case Constants.EN_COLUMN:
            return RtfTextrun.BREAK_COLUMN;
        default:
            return RtfTextrun.BREAK_NONE;
        }
    }

    /** {@inheritDoc} */
    public void startTable(Table tbl) {
        if (bDefer) {
            return;
        }

        // create an RtfTable in the current table container
        TableContext tableContext = new TableContext(builderContext);

        try {
            final IRtfTableContainer tc
                = (IRtfTableContainer)builderContext.getContainer(
                        IRtfTableContainer.class, true, null);

            RtfAttributes atts
                = TableAttributesConverter.convertTableAttributes(tbl);

            RtfTable table = tc.newTable(atts, tableContext);
            table.setNestedTableDepth(nestedTableDepth);
            nestedTableDepth++;

            CommonBorderPaddingBackground border = tbl.getCommonBorderPaddingBackground();
            RtfAttributes borderAttributes = new RtfAttributes();

            BorderAttributesConverter.makeBorder(border, CommonBorderPaddingBackground.BEFORE,
                    borderAttributes, ITableAttributes.CELL_BORDER_TOP);
            BorderAttributesConverter.makeBorder(border, CommonBorderPaddingBackground.AFTER,
                    borderAttributes, ITableAttributes.CELL_BORDER_BOTTOM);
            BorderAttributesConverter.makeBorder(border, CommonBorderPaddingBackground.START,
                    borderAttributes, ITableAttributes.CELL_BORDER_LEFT);
            BorderAttributesConverter.makeBorder(border, CommonBorderPaddingBackground.END,
                    borderAttributes,  ITableAttributes.CELL_BORDER_RIGHT);

            table.setBorderAttributes(borderAttributes);

            builderContext.pushContainer(table);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startTable:" + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }

        builderContext.pushTableContext(tableContext);
    }

    /** {@inheritDoc} */
    public void endTable(Table tbl) {
        if (bDefer) {
            return;
        }

        nestedTableDepth--;
        builderContext.popTableContext();
        builderContext.popContainer(RtfTable.class, this);
    }

    /** {@inheritDoc} */
    public void startColumn(TableColumn tc) {
        if (bDefer) {
            return;
        }

        try {
            int iWidth = tc.getColumnWidth().getValue(percentManager);
            percentManager.setDimension(tc, iWidth);

            //convert to twips
            Float width = FoUnitsConverter.getInstance().convertMptToTwips(iWidth);
            builderContext.getTableContext().setNextColumnWidth(width);
            builderContext.getTableContext().setNextColumnRowSpanning(
                    0, null);
            builderContext.getTableContext().setNextFirstSpanningCol(false);
        } catch (Exception e) {
            log.error("startColumn: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endColumn(TableColumn tc) {
    }

    /** {@inheritDoc} */
    public void startHeader(TableHeader header) {
        builderContext.pushPart(header);
        startPart(header);
    }

    /** {@inheritDoc} */
    public void endHeader(TableHeader header) {
        builderContext.popPart(header.getClass(), this);
        endPart(header);
    }

    /** {@inheritDoc} */
    public void startFooter(TableFooter footer) {
        builderContext.pushPart(footer);
        startPart(footer);
    }

    /** {@inheritDoc} */
    public void endFooter(TableFooter footer) {
        builderContext.popPart(footer.getClass(), this);
        endPart(footer);
    }

    /** {@inheritDoc} */
    public void startInline(Inline inl) {
        if (bDefer) {
            return;
        }

        try {
            RtfAttributes rtfAttr
                = TextAttributesConverter.convertCharacterAttributes(inl);

            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class, true, this);

            RtfTextrun textrun = container.getTextrun();
            textrun.pushInlineAttributes(rtfAttr);
            textrun.addBookmark(inl.getId());
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (FOPException fe) {
            log.error("startInline:" + fe.getMessage());
            throw new RuntimeException(fe.getMessage());
        } catch (Exception e) {
            log.error("startInline:" + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endInline(Inline inl) {
        if (bDefer) {
            return;
        }

        try {
            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class, true, this);

            RtfTextrun textrun = container.getTextrun();
            textrun.popInlineAttributes();
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startInline:" + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    private void startPart(TablePart part) {
        if (bDefer) {
            return;
        }

        try {
            RtfAttributes atts = TableAttributesConverter.convertTablePartAttributes(part);

            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            RtfTable tbl = (RtfTable)builderContext.getContainer(RtfTable.class, true, this);
            tbl.setHeaderAttribs(atts);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startPart: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    private void endPart(TablePart tb) {
        if (bDefer) {
            return;
        }

        try {
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            RtfTable tbl = (RtfTable)builderContext.getContainer(RtfTable.class, true, this);
            tbl.setHeaderAttribs(null);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("endPart: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }


     /**
     * {@inheritDoc}
     */
    public void startBody(TableBody body) {
        builderContext.pushPart(body);
        startPart(body);
    }

    /** {@inheritDoc} */
    public void endBody(TableBody body) {
        builderContext.popPart(TableBody.class, this);
        endPart(body);
    }

    /**
     * {@inheritDoc}
     */
    public void startRow(TableRow tr) {
        if (bDefer) {
            return;
        }

        try {
            // create an RtfTableRow in the current RtfTable
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            final RtfTable tbl = (RtfTable)builderContext.getContainer(RtfTable.class,
                    true, null);

            RtfAttributes atts = TableAttributesConverter.convertRowAttributes(tr,
                    tbl.getHeaderAttribs());

            if (tr.getParent() instanceof TableHeader) {
                atts.set(ITableAttributes.ATTR_HEADER);
            }

            builderContext.pushContainer(tbl.newTableRow(atts));

            // reset column iteration index to correctly access column widths
            builderContext.getTableContext().selectFirstColumn();
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startRow: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endRow(TableRow tr) {
        if (bDefer) {
            return;
        }

        try {
            TableContext tctx = builderContext.getTableContext();
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            final RtfTableRow row = (RtfTableRow)builderContext.getContainer(RtfTableRow.class,
                    true, null);

            //while the current column is in row-spanning, act as if
            //a vertical merged cell would have been specified.
            while (tctx.getNumberOfColumns() > tctx.getColumnIndex()
                  && tctx.getColumnRowSpanningNumber() > 0) {
                RtfTableCell vCell = row.newTableCellMergedVertically(
                        (int)tctx.getColumnWidth(),
                        tctx.getColumnRowSpanningAttrs());

                if (!tctx.getFirstSpanningCol()) {
                    vCell.setHMerge(RtfTableCell.MERGE_WITH_PREVIOUS);
                }

                tctx.selectNextColumn();
            }
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("endRow: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }


        builderContext.popContainer(RtfTableRow.class, this);
        builderContext.getTableContext().decreaseRowSpannings();
    }

    /** {@inheritDoc} */
    public void startCell(TableCell tc) {
        if (bDefer) {
            return;
        }

        try {
            TableContext tctx = builderContext.getTableContext();
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            final RtfTableRow row = (RtfTableRow)builderContext.getContainer(RtfTableRow.class,
                    true, null);

            int numberRowsSpanned = tc.getNumberRowsSpanned();
            int numberColumnsSpanned = tc.getNumberColumnsSpanned();

            //while the current column is in row-spanning, act as if
            //a vertical merged cell would have been specified.
            while (tctx.getNumberOfColumns() > tctx.getColumnIndex()
                  && tctx.getColumnRowSpanningNumber() > 0) {
                RtfTableCell vCell = row.newTableCellMergedVertically(
                        (int)tctx.getColumnWidth(),
                        tctx.getColumnRowSpanningAttrs());

                if (!tctx.getFirstSpanningCol()) {
                    vCell.setHMerge(RtfTableCell.MERGE_WITH_PREVIOUS);
                }

                tctx.selectNextColumn();
            }

            //get the width of the currently started cell
            float width = tctx.getColumnWidth();

            // create an RtfTableCell in the current RtfTableRow
            RtfAttributes atts = TableAttributesConverter.convertCellAttributes(tc);
            RtfTableCell cell = row.newTableCell((int)width, atts);

            //process number-rows-spanned attribute
            if (numberRowsSpanned > 1) {
                // Start vertical merge
                cell.setVMerge(RtfTableCell.MERGE_START);

                // set the number of rows spanned
                tctx.setCurrentColumnRowSpanning(numberRowsSpanned,
                        cell.getRtfAttributes());
            } else {
                tctx.setCurrentColumnRowSpanning(
                        numberRowsSpanned, null);
            }

            //process number-columns-spanned attribute
            if (numberColumnsSpanned > 0) {
                // Get the number of columns spanned
                tctx.setCurrentFirstSpanningCol(true);

                // We widthdraw one cell because the first cell is already created
                // (it's the current cell) !
                 for (int i = 0; i < numberColumnsSpanned - 1; ++i) {
                    tctx.selectNextColumn();

                    //aggregate width for further elements
                    width += tctx.getColumnWidth();
                    tctx.setCurrentFirstSpanningCol(false);
                    RtfTableCell hCell = row.newTableCellMergedHorizontally(
                            0, null);

                    if (numberRowsSpanned > 1) {
                        // Start vertical merge
                        hCell.setVMerge(RtfTableCell.MERGE_START);

                        // set the number of rows spanned
                        tctx.setCurrentColumnRowSpanning(
                                numberRowsSpanned,
                                cell.getRtfAttributes());
                    } else {
                        tctx.setCurrentColumnRowSpanning(
                                numberRowsSpanned, cell.getRtfAttributes());
                    }
                }
            }
            //save width of the cell, convert from twips to mpt
            percentManager.setDimension(tc, (int)width * 50);

            builderContext.pushContainer(cell);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startCell: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endCell(TableCell tc) {
        if (bDefer) {
            return;
        }
        try {
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            RtfTableCell cell = (RtfTableCell)builderContext.getContainer(RtfTableCell.class, false, this);
            cell.finish();

        } catch (Exception e) {
            log.error("endCell: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }

        builderContext.popContainer(RtfTableCell.class, this);
        builderContext.getTableContext().selectNextColumn();
    }

    // Lists
    /** {@inheritDoc} */
    public void startList(ListBlock lb) {
        if (bDefer) {
            return;
        }

        try  {
            // create an RtfList in the current list container
            final IRtfListContainer c
                = (IRtfListContainer)builderContext.getContainer(
                    IRtfListContainer.class, true, this);
            final RtfList newList = c.newList(
                ListAttributesConverter.convertAttributes(lb));
            builderContext.pushContainer(newList);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (FOPException fe) {
            log.error("startList: " + fe.getMessage());
            throw new RuntimeException(fe.getMessage());
        } catch (Exception e) {
            log.error("startList: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endList(ListBlock lb) {
        if (bDefer) {
            return;
        }

        builderContext.popContainer(RtfList.class, this);
    }

    /** {@inheritDoc} */
    public void startListItem(ListItem li) {
        if (bDefer) {
            return;
        }

        // create an RtfListItem in the current RtfList
        try {
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            RtfList list = (RtfList)builderContext.getContainer(
                    RtfList.class, true, this);

            /**
             * If the current list already contains a list item, then close the
             * list and open a new one, so every single list item gets its own
             * list. This allows every item to have a different list label.
             * If all the items would be in the same list, they had all the
             * same label.
             */
            //TODO: do this only, if the labels content <> previous labels content
            if (list.getChildCount() > 0) {
                this.endListBody(null);
                this.endList((ListBlock) li.getParent());
                this.startList((ListBlock) li.getParent());
                this.startListBody(null);

                list = (RtfList)builderContext.getContainer(
                        RtfList.class, true, this);
            }

            builderContext.pushContainer(list.newListItem());
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startList: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endListItem(ListItem li) {
        if (bDefer) {
            return;
        }

        builderContext.popContainer(RtfListItem.class, this);
    }

    /** {@inheritDoc} */
    public void startListLabel(ListItemLabel listItemLabel) {
        if (bDefer) {
            return;
        }

        try {
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            RtfListItem item
                = (RtfListItem)builderContext.getContainer(RtfListItem.class, true, this);

            RtfListItemLabel label = item.new RtfListItemLabel(item);
            builderContext.pushContainer(label);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startPageNumber: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endListLabel(ListItemLabel listItemLabel) {
        if (bDefer) {
            return;
        }

        builderContext.popContainer(RtfListItemLabel.class, this);
    }

    /** {@inheritDoc} */
    public void startListBody(ListItemBody listItemBody) {
    }

    /** {@inheritDoc} */
    public void endListBody(ListItemBody listItemBody) {
    }

    // Static Regions
    /** {@inheritDoc} */
    public void startStatic(StaticContent staticContent) {
    }

    /** {@inheritDoc} */
    public void endStatic(StaticContent statisContent) {
    }

    /** {@inheritDoc} */
    public void startMarkup() {
    }

    /** {@inheritDoc} */
    public void endMarkup() {
    }

    /** {@inheritDoc} */
    public void startLink(BasicLink basicLink) {
        if (bDefer) {
            return;
        }

        try {
            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class, true, this);

            RtfTextrun textrun = container.getTextrun();

            RtfHyperLink link = textrun.addHyperlink(new RtfAttributes());

            if (basicLink.hasExternalDestination()) {
                link.setExternalURL(basicLink.getExternalDestination());
            } else {
                link.setInternalURL(basicLink.getInternalDestination());
            }

            builderContext.pushContainer(link);

        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startLink: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endLink(BasicLink basicLink) {
        if (bDefer) {
            return;
        }

        builderContext.popContainer(RtfHyperLink.class, this);
    }

    /** {@inheritDoc} */
    public void image(ExternalGraphic eg) {
        if (bDefer) {
            return;
        }

        String uri = eg.getURL();
        ImageInfo info = null;
        try {

            //set image data
            FOUserAgent userAgent = eg.getUserAgent();
            ImageManager manager = userAgent.getImageManager();
            info = manager.getImageInfo(uri, userAgent.getImageSessionContext());

            putGraphic(eg, info);
        } catch (ImageException ie) {
            ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.imageError(this, uri, ie, null);
        } catch (FileNotFoundException fe) {
            ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.imageNotFound(this, (info != null ? info.toString() : uri), fe, null);
        } catch (IOException ioe) {
            ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.imageIOError(this, (info != null ? info.toString() : uri), ioe, null);
        }
    }

    /** {@inheritDoc} */
    public void endInstreamForeignObject(InstreamForeignObject ifo) {
        if (bDefer) {
            return;
        }

        try {
            XMLObj child = ifo.getChildXMLObj();
            Document doc = child.getDOMDocument();
            String ns = child.getNamespaceURI();

            ImageInfo info = new ImageInfo(null, null);
            // Set the resolution to that of the FOUserAgent
            FOUserAgent ua = ifo.getUserAgent();
            ImageSize size = new ImageSize();
            size.setResolution(ua.getSourceResolution());

            // Set the image size to the size of the svg.
            Point2D csize = new Point2D.Float(-1, -1);
            Point2D intrinsicDimensions = child.getDimension(csize);
            if (intrinsicDimensions == null) {
                ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                        getUserAgent().getEventBroadcaster());
                eventProducer.ifoNoIntrinsicSize(this, child.getLocator());
                return;
            }
            size.setSizeInMillipoints(
                    (int)Math.round(intrinsicDimensions.getX() * 1000),
                    (int)Math.round(intrinsicDimensions.getY() * 1000));
            size.calcPixelsFromSize();
            info.setSize(size);

            ImageXMLDOM image = new ImageXMLDOM(info, doc, ns);

            FOUserAgent userAgent = ifo.getUserAgent();
            ImageManager manager = userAgent.getImageManager();
            Map hints = ImageUtil.getDefaultHints(ua.getImageSessionContext());
            Image converted = manager.convertImage(image, FLAVORS, hints);
            putGraphic(ifo, converted);

        } catch (ImageException ie) {
            ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.imageError(this, null, ie, null);
        } catch (IOException ioe) {
            ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.imageIOError(this, null, ioe, null);
        }
    }

    private static final ImageFlavor[] FLAVORS = new ImageFlavor[] {
        ImageFlavor.RAW_EMF, ImageFlavor.RAW_PNG, ImageFlavor.RAW_JPEG
    };

    /**
     * Puts a graphic/image into the generated RTF file.
     * @param abstractGraphic the graphic (external-graphic or instream-foreign-object)
     * @param info the image info object
     * @throws IOException In case of an I/O error
     */
    private void putGraphic(AbstractGraphics abstractGraphic, ImageInfo info)
            throws IOException {
        try {
            FOUserAgent userAgent = abstractGraphic.getUserAgent();
            ImageManager manager = userAgent.getImageManager();
            ImageSessionContext sessionContext = userAgent.getImageSessionContext();
            Map hints = ImageUtil.getDefaultHints(sessionContext);
            Image image = manager.getImage(info, FLAVORS, hints, sessionContext);

            putGraphic(abstractGraphic, image);
        } catch (ImageException ie) {
            ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.imageError(this, null, ie, null);
        }
    }

    /**
     * Puts a graphic/image into the generated RTF file.
     * @param abstractGraphic the graphic (external-graphic or instream-foreign-object)
     * @param image the image
     * @throws IOException In case of an I/O error
     */
    private void putGraphic(AbstractGraphics abstractGraphic, Image image)
            throws IOException {
        byte[] rawData = null;

        final ImageInfo info = image.getInfo();

        if (image instanceof ImageRawStream) {
            ImageRawStream rawImage = (ImageRawStream)image;
            InputStream in = rawImage.createInputStream();
            try {
                rawData = IOUtils.toByteArray(in);
            } finally {
                IOUtils.closeQuietly(in);
            }
        }

        if (rawData == null) {
            ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.imageWritingError(this, null);
            return;
        }

        //Set up percentage calculations
        this.percentManager.setDimension(abstractGraphic);
        PercentBaseContext pContext = new PercentBaseContext() {

            public int getBaseLength(int lengthBase, FObj fobj) {
                switch (lengthBase) {
                case LengthBase.IMAGE_INTRINSIC_WIDTH:
                    return info.getSize().getWidthMpt();
                case LengthBase.IMAGE_INTRINSIC_HEIGHT:
                    return info.getSize().getHeightMpt();
                default:
                    return percentManager.getBaseLength(lengthBase, fobj);
                }
            }

        };
        ImageLayout layout = new ImageLayout(abstractGraphic, pContext,
                image.getInfo().getSize().getDimensionMpt());

        final IRtfTextrunContainer c
            = (IRtfTextrunContainer)builderContext.getContainer(
                IRtfTextrunContainer.class, true, this);

        final RtfExternalGraphic rtfGraphic = c.getTextrun().newImage();

        //set URL
        if (info.getOriginalURI() != null) {
            rtfGraphic.setURL(info.getOriginalURI());
        }
        rtfGraphic.setImageData(rawData);

        FoUnitsConverter converter = FoUnitsConverter.getInstance();
        Dimension viewport = layout.getViewportSize();
        Rectangle placement = layout.getPlacement();
        int cropLeft = Math.round(converter.convertMptToTwips(-placement.x));
        int cropTop = Math.round(converter.convertMptToTwips(-placement.y));
        int cropRight = Math.round(converter.convertMptToTwips(
                -1 * (viewport.width - placement.x - placement.width)));
        int cropBottom = Math.round(converter.convertMptToTwips(
                -1 * (viewport.height - placement.y - placement.height)));
        rtfGraphic.setCropping(cropLeft, cropTop, cropRight, cropBottom);

        int width = Math.round(converter.convertMptToTwips(viewport.width));
        int height = Math.round(converter.convertMptToTwips(viewport.height));
        width += cropLeft + cropRight;
        height += cropTop + cropBottom;
        rtfGraphic.setWidthTwips(width);
        rtfGraphic.setHeightTwips(height);

        //TODO: make this configurable:
        //      int compression = m_context.m_options.getRtfExternalGraphicCompressionRate ();
        int compression = 0;
        if (compression != 0) {
            if (!rtfGraphic.setCompressionRate(compression)) {
                log.warn("The compression rate " + compression
                    + " is invalid. The value has to be between 1 and 100 %.");
            }
        }
    }

    /** {@inheritDoc} */
    public void pageRef() {
    }

    /** {@inheritDoc} */
    public void startFootnote(Footnote footnote) {
        if (bDefer) {
            return;
        }

        try {
            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class,
                    true, this);

            RtfTextrun textrun = container.getTextrun();
            RtfFootnote rtfFootnote = textrun.addFootnote();

            builderContext.pushContainer(rtfFootnote);

        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startFootnote: " + e.getMessage());
            throw new RuntimeException("Exception: " + e);
        }
    }

    /** {@inheritDoc} */
    public void endFootnote(Footnote footnote) {
        if (bDefer) {
            return;
        }

        builderContext.popContainer(RtfFootnote.class, this);
    }

    /** {@inheritDoc} */
    public void startFootnoteBody(FootnoteBody body) {
        if (bDefer) {
            return;
        }

        try {
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            RtfFootnote rtfFootnote
                = (RtfFootnote)builderContext.getContainer(
                    RtfFootnote.class,
                    true, this);

            rtfFootnote.startBody();
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startFootnoteBody: " + e.getMessage());
            throw new RuntimeException("Exception: " + e);
        }
    }

    /** {@inheritDoc} */
    public void endFootnoteBody(FootnoteBody body) {
        if (bDefer) {
            return;
        }

        try {
            // @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
            RtfFootnote rtfFootnote
                = (RtfFootnote)builderContext.getContainer(
                    RtfFootnote.class,
                    true, this);

            rtfFootnote.endBody();
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("endFootnoteBody: " + e.getMessage());
            throw new RuntimeException("Exception: " + e);
        }
    }

    /** {@inheritDoc} */
    public void startLeader(Leader l) {
        if (bDefer) {
            return;
        }

        try {
            percentManager.setDimension(l);
            RtfAttributes rtfAttr = TextAttributesConverter.convertLeaderAttributes(
                    l, percentManager);

            IRtfTextrunContainer container
                  = (IRtfTextrunContainer)builderContext.getContainer(
                      IRtfTextrunContainer.class, true, this);
            RtfTextrun textrun = container.getTextrun();

            textrun.addLeader(rtfAttr);
        } catch (IOException e) {
            log.error("startLeader: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        } catch (FOPException e) {
            log.error("startLeader: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * @param text FOText object
     * @param characters CharSequence of the characters to process.
     */
    public void text(FOText text, CharSequence characters) {
        if (bDefer) {
            return;
        }

        try {
            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class, true, this);

            RtfTextrun textrun = container.getTextrun();
            RtfAttributes rtfAttr
                = TextAttributesConverter.convertCharacterAttributes(text);

            textrun.pushInlineAttributes(rtfAttr);
            textrun.addString(characters.toString());
            textrun.popInlineAttributes();
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("characters:" + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void startPageNumber(PageNumber pagenum) {
        if (bDefer) {
            return;
        }

        try {
            RtfAttributes rtfAttr
                = TextAttributesConverter.convertCharacterAttributes(
                    pagenum);

            IRtfTextrunContainer container
                = (IRtfTextrunContainer)builderContext.getContainer(
                    IRtfTextrunContainer.class, true, this);

            RtfTextrun textrun = container.getTextrun();
            textrun.addPageNumber(rtfAttr);
        } catch (IOException ioe) {
            handleIOTrouble(ioe);
        } catch (Exception e) {
            log.error("startPageNumber: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void endPageNumber(PageNumber pagenum) {
    }

    /** {@inheritDoc} */
    public void startPageNumberCitation(PageNumberCitation l) {
        if (bDefer) {
            return;
        }
        try {

            IRtfTextrunContainer container
                  = (IRtfTextrunContainer)builderContext.getContainer(
                      IRtfTextrunContainer.class, true, this);
            RtfTextrun textrun = container.getTextrun();

            textrun.addPageNumberCitation(l.getRefId());

        } catch (Exception e) {
            log.error("startPageNumberCitation: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    /** {@inheritDoc} */
    public void startPageNumberCitationLast(PageNumberCitationLast l) {
        if (bDefer) {
            return;
        }
        try {

            IRtfTextrunContainer container
                  = (IRtfTextrunContainer)builderContext.getContainer(
                      IRtfTextrunContainer.class, true, this);
            RtfTextrun textrun = container.getTextrun();

            textrun.addPageNumberCitation(l.getRefId());

        } catch (RtfException e) {
            log.error("startPageNumberCitationLast: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        } catch (IOException e) {
            log.error("startPageNumberCitationLast: " + e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    private void prepareTable(Table tab) {
        // Allows to receive the available width of the table
        percentManager.setDimension(tab);

        // Table gets expanded by half of the border on each side inside Word
        // When using wide borders the table gets cut off
        int tabDiff = tab.getCommonBorderPaddingBackground().getBorderStartWidth(false) / 2
                + tab.getCommonBorderPaddingBackground().getBorderEndWidth(false);

        // check for "auto" value
        if (!(tab.getInlineProgressionDimension().getMaximum(null).getLength()
                    instanceof EnumLength)) {
            // value specified
            percentManager.setDimension(tab,
                    tab.getInlineProgressionDimension().getMaximum(null)
                        .getLength().getValue(percentManager)
                    - tabDiff);
        } else {
            // set table width again without border width
            percentManager.setDimension(tab, percentManager.getBaseLength(
                    LengthBase.CONTAINING_BLOCK_WIDTH, tab) - tabDiff);
        }

        ColumnSetup columnSetup = new ColumnSetup(tab);
        //int sumOfColumns = columnSetup.getSumOfColumnWidths(percentManager);
        float tableWidth = percentManager.getBaseLength(LengthBase.CONTAINING_BLOCK_WIDTH, tab);
        float tableUnit = columnSetup.computeTableUnit(percentManager, Math.round(tableWidth));
        percentManager.setTableUnit(tab, Math.round(tableUnit));

    }

    /**
     * Calls the appropriate event handler for the passed FObj.
     *
     * @param foNode FO node whose event is to be called
     * @param bStart TRUE calls the start handler, FALSE the end handler
     */
    private void invokeDeferredEvent(FONode foNode, boolean bStart) {
        if (foNode instanceof PageSequence) {
            if (bStart) {
                startPageSequence((PageSequence) foNode);
            } else {
                endPageSequence((PageSequence) foNode);
            }
        } else if (foNode instanceof Flow) {
            if (bStart) {
                startFlow((Flow) foNode);
            } else {
                endFlow((Flow) foNode);
            }
        } else if (foNode instanceof StaticContent) {
            if (bStart) {
                startStatic(null);
            } else {
                endStatic(null);
            }
        } else if (foNode instanceof ExternalGraphic) {
            if (bStart) {
                image((ExternalGraphic) foNode);
            }
        } else if (foNode instanceof InstreamForeignObject) {
            if (bStart) {
                endInstreamForeignObject((InstreamForeignObject) foNode);
            }
        } else if (foNode instanceof Block) {
            if (bStart) {
                startBlock((Block) foNode);
            } else {
                endBlock((Block) foNode);
            }
        } else if (foNode instanceof BlockContainer) {
            if (bStart) {
                startBlockContainer((BlockContainer) foNode);
            } else {
                endBlockContainer((BlockContainer) foNode);
            }
        } else if (foNode instanceof BasicLink) {
            //BasicLink must be placed before Inline
            if (bStart) {
                startLink((BasicLink) foNode);
            } else {
                endLink(null);
            }
        } else if (foNode instanceof Inline) {
            if (bStart) {
                startInline((Inline) foNode);
            } else {
                endInline((Inline) foNode);
            }
        } else if (foNode instanceof FOText) {
            if (bStart) {
                FOText text = (FOText) foNode;
                text(text, text.getCharSequence());
            }
        } else if (foNode instanceof Character) {
            if (bStart) {
                Character c = (Character) foNode;
                character(c);
            }
        } else if (foNode instanceof PageNumber) {
            if (bStart) {
                startPageNumber((PageNumber) foNode);
            } else {
                endPageNumber((PageNumber) foNode);
            }
        } else if (foNode instanceof Footnote) {
            if (bStart) {
                startFootnote((Footnote) foNode);
            } else {
                endFootnote((Footnote) foNode);
            }
        } else if (foNode instanceof FootnoteBody) {
            if (bStart) {
                startFootnoteBody((FootnoteBody) foNode);
            } else {
                endFootnoteBody((FootnoteBody) foNode);
            }
        } else if (foNode instanceof ListBlock) {
            if (bStart) {
                startList((ListBlock) foNode);
            } else {
                endList((ListBlock) foNode);
            }
        } else if (foNode instanceof ListItemBody) {
            if (bStart) {
                startListBody(null);
            } else {
                endListBody(null);
            }
        } else if (foNode instanceof ListItem) {
            if (bStart) {
                startListItem((ListItem) foNode);
            } else {
                endListItem((ListItem) foNode);
            }
        } else if (foNode instanceof ListItemLabel) {
            if (bStart) {
                startListLabel(null);
            } else {
                endListLabel(null);
            }
        } else if (foNode instanceof Table) {
            if (bStart) {
                startTable((Table) foNode);
            } else {
                endTable((Table) foNode);
            }
        } else if (foNode instanceof TableHeader) {
            if (bStart) {
                startHeader((TableHeader) foNode);
            } else {
                endHeader((TableHeader) foNode);
            }
        } else if (foNode instanceof TableFooter) {
            if (bStart) {
                startFooter((TableFooter) foNode);
            } else {
                endFooter((TableFooter) foNode);
            }
        } else if (foNode instanceof TableBody) {
            if (bStart) {
                startBody((TableBody) foNode);
            } else {
                endBody((TableBody) foNode);
            }
        } else if (foNode instanceof TableColumn) {
            if (bStart) {
                startColumn((TableColumn) foNode);
            } else {
                endColumn((TableColumn) foNode);
            }
        } else if (foNode instanceof TableRow) {
            if (bStart) {
                startRow((TableRow) foNode);
            } else {
                endRow((TableRow) foNode);
            }
        } else if (foNode instanceof TableCell) {
            if (bStart) {
                startCell((TableCell) foNode);
            } else {
                endCell((TableCell) foNode);
            }
        } else if (foNode instanceof Leader) {
            if (bStart) {
                startLeader((Leader) foNode);
            }
        } else if (foNode instanceof PageNumberCitation) {
            if (bStart) {
                startPageNumberCitation((PageNumberCitation) foNode);
            } else {
                endPageNumberCitation((PageNumberCitation) foNode);
            }
        } else if (foNode instanceof PageNumberCitationLast) {
            if (bStart) {
                startPageNumberCitationLast((PageNumberCitationLast) foNode);
            } else {
                endPageNumberCitationLast((PageNumberCitationLast) foNode);
            }
        } else {
            RTFEventProducer eventProducer = RTFEventProducer.Provider.get(
                    getUserAgent().getEventBroadcaster());
            eventProducer.ignoredDeferredEvent(this, foNode, bStart, foNode.getLocator());
        }
    }

    /**
     * Closes any mismatched tags that are detected in the RTF structure.
     * @param containerClass The class representing the tag to close.
     * @return Determines whether the tag mismatch has been handled.
     */
    public boolean endContainer(Class containerClass) {
        if (containerClass == RtfTableRow.class) {
            endRow(null);
            return true;
        }
        return false;
    }

    /**
     * Calls the event handlers for the passed FONode and all its elements.
     *
     * @param foNode FONode object which shall be recursed
     */
    private void recurseFONode(FONode foNode) {
        invokeDeferredEvent(foNode, true);

        if (foNode instanceof PageSequence) {
            PageSequence pageSequence = (PageSequence) foNode;

            Region regionBefore = pagemaster.getRegion(Constants.FO_REGION_BEFORE);
            if (regionBefore != null) {
                FONode staticBefore = pageSequence.getFlowMap().get(
                        regionBefore.getRegionName());
                if (staticBefore != null) {
                    recurseFONode(staticBefore);
                }
            }
            Region regionAfter = pagemaster.getRegion(Constants.FO_REGION_AFTER);
            if (regionAfter != null) {
                FONode staticAfter = pageSequence.getFlowMap().get(
                        regionAfter.getRegionName());
                if (staticAfter != null) {
                    recurseFONode(staticAfter);
                }
            }


            recurseFONode(pageSequence.getMainFlow());
        } else if (foNode instanceof Table) {
            Table table = (Table) foNode;

            //recurse all table-columns
            if (table.getColumns() != null) {
              //Calculation for column-widths which are not set
              prepareTable(table);

                for (Object o : table.getColumns()) {
                    recurseFONode((FONode) o);
                }
            } else {
                //TODO Implement implicit column setup handling!
                RTFEventProducer eventProducer = RTFEventProducer.Provider.get(
                        getUserAgent().getEventBroadcaster());
                eventProducer.explicitTableColumnsRequired(this, table.getLocator());
            }

            //recurse table-header
            if (table.getTableHeader() != null) {
                recurseFONode(table.getTableHeader());
            }

            //recurse table-footer
            if (table.getTableFooter() != null) {
                recurseFONode(table.getTableFooter());
            }

            if (foNode.getChildNodes() != null) {
                for (Iterator it = foNode.getChildNodes(); it.hasNext();) {
                    recurseFONode((FONode) it.next());
                }
            }
        } else if (foNode instanceof ListItem) {
            ListItem item = (ListItem) foNode;

            recurseFONode(item.getLabel());
            recurseFONode(item.getBody());
        } else if (foNode instanceof Footnote) {
            Footnote fn = (Footnote)foNode;

            recurseFONode(fn.getFootnoteCitation());
            recurseFONode(fn.getFootnoteBody());
        } else {
            //Any other FO-Object: Simply recurse through all childNodes.
            if (foNode.getChildNodes() != null) {
                for (Iterator it = foNode.getChildNodes(); it.hasNext();) {
                    FONode fn = (FONode)it.next();
                    if (log.isTraceEnabled()) {
                        log.trace("  ChildNode for " + fn + " (" + fn.getName() + ")");
                    }
                    recurseFONode(fn);
                }
            }
        }

        invokeDeferredEvent(foNode, false);
    }
}
