/*
 * 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: AbstractRenderer.java 1835810 2018-07-13 10:29:57Z ssteiner $ */

package org.apache.fop.render;

// Java
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Stack;

import org.w3c.dom.Document;

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

import org.apache.fop.ResourceEventProducer;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.area.Area;
import org.apache.fop.area.BeforeFloat;
import org.apache.fop.area.Block;
import org.apache.fop.area.BlockParent;
import org.apache.fop.area.BlockViewport;
import org.apache.fop.area.BodyRegion;
import org.apache.fop.area.CTM;
import org.apache.fop.area.Footnote;
import org.apache.fop.area.LineArea;
import org.apache.fop.area.MainReference;
import org.apache.fop.area.NormalFlow;
import org.apache.fop.area.OffDocumentItem;
import org.apache.fop.area.Page;
import org.apache.fop.area.PageSequence;
import org.apache.fop.area.PageViewport;
import org.apache.fop.area.RegionReference;
import org.apache.fop.area.RegionViewport;
import org.apache.fop.area.Span;
import org.apache.fop.area.Trait;
import org.apache.fop.area.inline.Container;
import org.apache.fop.area.inline.FilledArea;
import org.apache.fop.area.inline.ForeignObject;
import org.apache.fop.area.inline.Image;
import org.apache.fop.area.inline.InlineArea;
import org.apache.fop.area.inline.InlineBlock;
import org.apache.fop.area.inline.InlineBlockParent;
import org.apache.fop.area.inline.InlineParent;
import org.apache.fop.area.inline.InlineViewport;
import org.apache.fop.area.inline.Leader;
import org.apache.fop.area.inline.Space;
import org.apache.fop.area.inline.SpaceArea;
import org.apache.fop.area.inline.TextArea;
import org.apache.fop.area.inline.WordArea;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.flow.ChangeBar;
import org.apache.fop.fo.properties.Property;
import org.apache.fop.fonts.FontInfo;
import org.apache.fop.traits.BorderProps;
import org.apache.fop.traits.Direction;
import org.apache.fop.traits.Visibility;

/**
 * Abstract base class for all renderers. The Abstract renderer does all the
 * top level processing of the area tree and adds some abstract methods to
 * handle viewports. This keeps track of the current block and inline position.
 */
public abstract class AbstractRenderer
         implements Renderer, Constants {

    /** logging instance */
    protected static final Log log = LogFactory.getLog("org.apache.fop.render");

    /**
     * user agent
     */
    protected FOUserAgent userAgent;

    /**
     * block progression position
     */
    protected int currentBPPosition;

    /**
     * inline progression position
     */
    protected int currentIPPosition;

    /**
     * the block progression position of the containing block used for
     * absolutely positioned blocks
     */
    protected int containingBPPosition;

    /**
     * the inline progression position of the containing block used for
     * absolutely positioned blocks
     */
    protected int containingIPPosition;

    /**
     * The "start edge" IP Position of the current column (for change bars)
     */
    protected int columnStartIPPosition;

    /**
     * The "end edge" IP Position of the current column (for change bars)
     */
    protected int columnEndIPPosition;

    /**
     * The "left" position of the current column (for change bars)
     */
    protected int columnLeftIPPosition;

    /**
     * The "right" position of the current column (for change bars)
     */
    protected int columnRightIPPosition;

    /**
     * The number of columns in the span (for change bars)
     */
    protected int columnCount;

    /**
     * The index number of the current column (for change bars)
     */
    protected int columnIndex;

    /**
     * The column width (for change bars)
     */
    protected int columnWidth;

    /**
     * The size of column gap (for change bars)
     */
    protected int columnGap;

    /**
     * The block progression direction (for change bars)
     */
    protected Direction blockProgressionDirection;

    /**
     * The inline progression direction (for change bars)
     */
    protected Direction inlineProgressionDirection;

    /**
     * Is binding on start edge of column?
     */
    protected boolean bindingOnStartEdge;

    /**
     * Is binding on end edge of column?
     */
    protected boolean bindingOnEndEdge;

    /**
     * The IP begin offset of coordinate 0
     */
    private int beginOffset;

    /**
     * the currently active PageViewport
     */
    protected PageViewport currentPageViewport;

    /* warned XML handlers */
    private Set warnedXMLHandlers;

    /* layers stack */
    private Stack<String> layers;

    /** {@inheritDoc} */
    public abstract void setupFontInfo(FontInfo fontInfo) throws FOPException;

    /**
     *
     * @param userAgent the user agent that contains configuration details. This cannot be null.
     */
    public AbstractRenderer(FOUserAgent userAgent) {
        this.userAgent = userAgent;
    }

    /** {@inheritDoc} */
    public FOUserAgent getUserAgent() {
        return userAgent;
    }

    /** {@inheritDoc} */
    public void startRenderer(OutputStream outputStream)
            throws IOException {
        if (userAgent == null) {
            throw new IllegalStateException("FOUserAgent has not been set on Renderer");
        }
    }

    /** {@inheritDoc} */
    public void stopRenderer()
        throws IOException { }

    /**
     * Check if this renderer supports out of order rendering. If this renderer
     * supports out of order rendering then it means that the pages that are
     * not ready will be prepared and a future page will be rendered.
     *
     * @return   True if the renderer supports out of order rendering
     */
    public boolean supportsOutOfOrder() {
        return false;
    }

    /** {@inheritDoc} */
    public void setDocumentLocale(Locale locale) {
    }

    /**
     * {@inheritDoc}
     */
    public void processOffDocumentItem(OffDocumentItem odi) { }

    /** {@inheritDoc} */
    public Graphics2DAdapter getGraphics2DAdapter() {
        return null;
    }

    /** {@inheritDoc} */
    public ImageAdapter getImageAdapter() {
        return null;
    }

    /** @return the current PageViewport or null, if none is active */
    protected PageViewport getCurrentPageViewport() {
        return this.currentPageViewport;
    }

    /** {@inheritDoc} */
    public void preparePage(PageViewport page) { }

    /**
     * Utility method to convert a page sequence title to a string. Some
     * renderers may only be able to use a string title. A title is a sequence
     * of inline areas that this method attempts to convert to an equivalent
     * string.
     *
     * @param title  The Title to convert
     * @return       An expanded string representing the title
     */
    protected String convertTitleToString(LineArea title) {
        List children = title.getInlineAreas();
        String str = convertToString(children);
        return str.trim();
    }

    private String convertToString(List children) {
        StringBuffer sb = new StringBuffer();
        for (Object aChildren : children) {
            InlineArea inline = (InlineArea) aChildren;
            //if (inline instanceof Character) {
            //    sb.append(((Character) inline).getChar());
            /*} else*/
            if (inline instanceof TextArea) {
                sb.append(((TextArea) inline).getText());
            } else if (inline instanceof InlineParent) {
                sb.append(convertToString(
                        ((InlineParent) inline).getChildAreas()));
            } else {
                sb.append(" ");
            }
        }
        return sb.toString();
    }

    /**
     * {@inheritDoc}
     * @deprecated
     */
    public void startPageSequence(LineArea seqTitle) {
        //do nothing
    }

    /** {@inheritDoc} */
    public void startPageSequence(PageSequence pageSequence) {
        // do nothing
    }

    // normally this would be overriden to create a page in the
    // output
    /** {@inheritDoc} */
    public void renderPage(PageViewport page)
        throws IOException, FOPException {

        this.currentPageViewport = page;
        try {
            Page p = page.getPage();
            renderPageAreas(p);
        } finally {
            this.currentPageViewport = null;
        }
    }

    /**
     * Renders page areas.
     *
     * @param page  The page whos page areas are to be rendered
     */
    protected void renderPageAreas(Page page) {
        /* Spec does not appear to specify whether fo:region-body should
        appear above or below side regions in cases of overlap.  FOP
        decision is to have fo:region-body on top, hence it is rendered
        last here. */
        RegionViewport viewport;
        viewport = page.getRegionViewport(FO_REGION_BEFORE);
        if (viewport != null) {
            renderRegionViewport(viewport);
        }
        viewport = page.getRegionViewport(FO_REGION_START);
        if (viewport != null) {
            renderRegionViewport(viewport);
        }
        viewport = page.getRegionViewport(FO_REGION_BODY);
        if (viewport != null) {
            renderRegionViewport(viewport);
        }
        viewport = page.getRegionViewport(FO_REGION_END);
        if (viewport != null) {
            renderRegionViewport(viewport);
        }
        viewport = page.getRegionViewport(FO_REGION_AFTER);
        if (viewport != null) {
            renderRegionViewport(viewport);
        }
    }

    /**
     * Renders a region viewport. <p>
     *
     * The region may clip the area and it establishes a position from where
     * the region is placed.</p>
     *
     * @param port  The region viewport to be rendered
     */
    protected void renderRegionViewport(RegionViewport port) {
        // The CTM will transform coordinates relative to
        // this region-reference area into page coords, so
        // set origin for the region to 0,0.
        currentBPPosition = 0;
        currentIPPosition = 0;

        RegionReference regionReference = port.getRegionReference();
        handleRegionTraits(port);

        //  shouldn't the viewport have the CTM
        startVParea(regionReference.getCTM(), port.getClipRectangle());
        // do after starting viewport area
        if (regionReference.getRegionClass() == FO_REGION_BODY) {
            assert (regionReference instanceof BodyRegion);
            renderBodyRegion((BodyRegion) regionReference);
        } else {
            renderRegion(regionReference);
        }
        endVParea();
    }

    /**
     * Establishes a new viewport area.
     *
     * @param ctm the coordinate transformation matrix to use
     * @param clippingRect the clipping rectangle if the viewport should be clipping,
     *                     null if no clipping is performed.
     */
    protected abstract void startVParea(CTM ctm, Rectangle clippingRect);

    /**
     * Signals exit from a viewport area. Subclasses can restore transformation matrices
     * valid before the viewport area was started.
     */
    protected abstract void endVParea();

    /**
     * Handle the traits for a region
     * This is used to draw the traits for the given page region.
     * (See Sect. 6.4.1.2 of XSL-FO spec.)
     * @param rv the RegionViewport whose region is to be drawn
     */
    protected void handleRegionTraits(RegionViewport rv) {
        // draw border and background
    }

    /**
     * Renders a region reference area.
     *
     * @param region  The region reference area
     */
    protected void renderRegion(RegionReference region) {
        renderBlocks(null, region.getBlocks());
    }

    /**
     * Renders a body region area.
     *
     * @param region  The body region
     */
    protected void renderBodyRegion(BodyRegion region) {
        BeforeFloat bf = region.getBeforeFloat();
        if (bf != null) {
            renderBeforeFloat(bf);
        }
        MainReference mr = region.getMainReference();
        if (mr != null) {
            renderMainReference(mr);
        }
        Footnote foot = region.getFootnote();
        if (foot != null) {
            renderFootnote(foot);
        }
    }

    /**
     * Renders a before float area.
     *
     * @param bf  The before float area
     */
    protected void renderBeforeFloat(BeforeFloat bf) {
        List blocks = bf.getChildAreas();
        if (blocks != null) {
            renderBlocks(null, blocks);
            Block sep = bf.getSeparator();
            if (sep != null) {
                renderBlock(sep);
            }
        }
    }

    /**
     * Renders a footnote
     *
     * @param footnote  The footnote
     */
    protected void renderFootnote(Footnote footnote) {
        currentBPPosition += footnote.getTop();
        List blocks = footnote.getChildAreas();
        if (blocks != null) {
            Block sep = footnote.getSeparator();
            if (sep != null) {
                renderBlock(sep);
            }
            renderBlocks(null, blocks);
        }
    }

    /**
     * Renders the main reference area.
     * <p>
     * The main reference area contains a list of spans that are
     * stacked on the page.
     * The spans contain a list of normal flow reference areas
     * that are positioned into columns.
     * </p>
     *
     * @param mainReference The main reference area
     */
    protected void renderMainReference(MainReference mainReference) {
        Span span = null;
        List spans = mainReference.getSpans();
        int saveBPPos = currentBPPosition;
        int saveIPPos = currentIPPosition;
        int saveSpanBPPos = saveBPPos;

        for (Object span1 : spans) {
            span = (Span) span1;

            columnCount = span.getColumnCount();
            columnGap = span.getColumnGap();
            columnWidth = span.getColumnWidth();

            blockProgressionDirection = (Direction) span.getTrait(Trait.BLOCK_PROGRESSION_DIRECTION);
            inlineProgressionDirection = (Direction) span.getTrait(Trait.INLINE_PROGRESSION_DIRECTION);

            int level = span.getBidiLevel();
            if (level < 0) {
                level = 0;
            }
            if ((level & 1) == 1) {
                currentIPPosition += span.getIPD();
                currentIPPosition += columnGap;
            }

            for (columnIndex = 0; columnIndex < columnCount; columnIndex++) {

                NormalFlow flow = span.getNormalFlow(columnIndex);

                boolean isLeftToRight = (inlineProgressionDirection == null)
                        || (inlineProgressionDirection.getEnumValue() == Constants.EN_LR);

                if (flow != null) {

                    // if direction is right to left, then end is left edge,
                    // else end is right edge (for top-bottom/bottom-top block
                    // progression directions)

                    // binding edge is on left edge for odd pages and
                    // on right edge for even pages

                    int pageIndex = currentPageViewport.getPageIndex();

                    bindingOnStartEdge = false;
                    bindingOnEndEdge = false;

                    if (isLeftToRight) {

                        columnStartIPPosition = 0;
                        columnEndIPPosition = columnWidth;
                        columnLeftIPPosition = 0;
                        columnRightIPPosition = columnWidth;

                        if (blockProgressionDirection == null || blockProgressionDirection.isVertical()) {
                            if (pageIndex % 2 == 0) {
                                bindingOnStartEdge = true;
                            } else {
                                bindingOnEndEdge = true;
                            }
                        }

                    } else {

                        columnStartIPPosition = columnWidth;
                        columnEndIPPosition = 0;
                        columnLeftIPPosition = 0;
                        columnRightIPPosition = columnWidth;

                        if (blockProgressionDirection == null || blockProgressionDirection.isVertical()) {
                            if (pageIndex % 2 == 0) {
                                bindingOnEndEdge = true;
                            } else {
                                bindingOnStartEdge = true;
                            }
                        }
                    }

                    currentBPPosition = saveSpanBPPos;
                    if ((level & 1) == 1) {
                        currentIPPosition -= flow.getIPD();
                        currentIPPosition -= columnGap;
                    }
                    renderFlow(flow);
                    if ((level & 1) == 0) {
                        currentIPPosition += flow.getIPD();
                        currentIPPosition += columnGap;
                    }
                }
            }
            currentIPPosition = saveIPPos;
            currentBPPosition = saveSpanBPPos + span.getHeight();
            saveSpanBPPos = currentBPPosition;
        }
        currentBPPosition = saveBPPos;
    }

    /**
     * Renders a flow reference area.
     *
     * @param flow  The flow reference area
     */
    protected void renderFlow(NormalFlow flow) {
        // the normal flow reference area contains stacked blocks
        List blocks = flow.getChildAreas();
        if (blocks != null) {
            renderBlocks(null, blocks);
        }
    }

    /**
     * Handle block traits.
     * This method is called when the correct ip and bp posiiton is
     * set. This should be overridden to draw border and background
     * traits for the block area.
     *
     * @param block the block area
     */
    protected void handleBlockTraits(Block block) {
        // draw border and background
    }

    /**
     * Renders a block viewport.
     *
     * @param bv        The block viewport
     * @param children  The children to render within the block viewport
     */
    protected void renderBlockViewport(BlockViewport bv, List children) {
        boolean inNewLayer = false;
        if (maybeStartLayer(bv)) {
            inNewLayer = true;
        }
        // clip and position viewport if necessary
        if (bv.getPositioning() == Block.ABSOLUTE) {
            // save positions
            int saveIP = currentIPPosition;
            int saveBP = currentBPPosition;

            Rectangle clippingRect = null;
            if (bv.hasClip()) {
                clippingRect = new Rectangle(saveIP, saveBP, bv.getIPD(), bv.getBPD());
            }

            CTM ctm = bv.getCTM();
            currentIPPosition = 0;
            currentBPPosition = 0;

            startVParea(ctm, clippingRect);
            handleBlockTraits(bv);
            renderBlocks(bv, children);
            endVParea();

            // clip if necessary
            currentIPPosition = saveIP;
            currentBPPosition = saveBP;
        } else {
            // save position and offset
            int saveIP = currentIPPosition;
            int saveBP = currentBPPosition;

            handleBlockTraits(bv);
            renderBlocks(bv, children);

            currentIPPosition = saveIP;
            currentBPPosition = saveBP + bv.getAllocBPD();
        }
        maybeEndLayer(bv, inNewLayer);
    }

    /**
     * Renders a block area that represents a reference area. The reference area establishes
     * a new coordinate system.
     * @param block the block area
     */
    protected abstract void renderReferenceArea(Block block);

    /**
     * Renders a list of block areas.
     *
     * @param parent  the parent block if the parent is a block, otherwise
     *                a null value.
     * @param blocks  The block areas
     */
    protected void renderBlocks(Block parent, List blocks) {
        int saveIP = currentIPPosition;

        // Calculate the position of the content rectangle.
        if (parent != null && !parent.getTraitAsBoolean(Trait.IS_VIEWPORT_AREA)) {
            currentBPPosition += parent.getBorderAndPaddingWidthBefore();
        }

        // the position of the containing block is used for
        // absolutely positioned areas
        int contBP = currentBPPosition;
        int contIP = currentIPPosition;
        containingBPPosition = currentBPPosition;
        containingIPPosition = currentIPPosition;

        for (Object obj : blocks) {
            if (obj instanceof Block) {
                currentIPPosition = contIP;
                containingBPPosition = contBP;
                containingIPPosition = contIP;
                renderBlock((Block) obj);
                containingBPPosition = contBP;
                containingIPPosition = contIP;
            } else if (obj instanceof LineArea) {
                // a line area is rendered from the top left position
                // of the line, each inline object is offset from there
                LineArea line = (LineArea) obj;
                if (parent != null) {
                    int level = parent.getBidiLevel();
                    if ((level == -1) || ((level & 1) == 0)) {
                        currentIPPosition += parent.getStartIndent();
                    } else {
                        currentIPPosition += parent.getEndIndent();
                    }
                }
                renderLineArea(line);
                currentBPPosition += line.getAllocBPD();
            }
            currentIPPosition = saveIP;
        }
    }

    /**
     * Renders a block area.
     *
     * @param block  The block area
     */
    protected void renderBlock(Block block) {
        assert block != null;
        List<ChangeBar> changeBarList = block.getChangeBarList();

        if (changeBarList != null && !changeBarList.isEmpty()) {
            int saveIP = currentIPPosition;
            int saveBP = currentBPPosition;

            drawChangeBars(block, changeBarList);

            currentIPPosition = saveIP;
            currentBPPosition = saveBP;
        }

        List children = block.getChildAreas();
        boolean inNewLayer = false;
        if (maybeStartLayer(block)) {
            inNewLayer = true;
        }
        if (block instanceof BlockViewport) {
            if (children != null) {
                renderBlockViewport((BlockViewport) block, children);
            } else {
                handleBlockTraits(block);
                // simply move position
                currentBPPosition += block.getAllocBPD();
            }
        } else if (block.getTraitAsBoolean(Trait.IS_REFERENCE_AREA)) {
            renderReferenceArea(block);
        } else {
            // save position and offset
            int saveIP = currentIPPosition;
            int saveBP = currentBPPosition;

            currentIPPosition += block.getXOffset();
            currentBPPosition += block.getYOffset();
            currentBPPosition += block.getSpaceBefore();

            handleBlockTraits(block);

            if (children != null && block.getTrait(Trait.VISIBILITY) != Visibility.HIDDEN)
            {
                renderBlocks(block, children);
            }

            if (block.getPositioning() == Block.ABSOLUTE) {
                // absolute blocks do not effect the layout
                currentBPPosition = saveBP;
            } else {
                // stacked and relative blocks effect stacking
                currentIPPosition = saveIP;
                currentBPPosition = saveBP + block.getAllocBPD();
            }
        }
        maybeEndLayer(block, inNewLayer);
    }

    /**
     * Renders an inline block area.
     *
     * @param inlineBlock The inline block area
     */
    protected void renderInlineBlock(InlineBlock inlineBlock) {
        renderBlock(inlineBlock.getBlock());
    }

    /**
     * Establish new optional content group layer.
     *
     * @param layer name of layer
     */
    protected abstract void startLayer(String layer);

    /**
     * Finish current optional content group layer.
     */
    protected abstract void endLayer();

    protected boolean maybeStartLayer(Area area) {
        String layer = (String) area.getTrait(Trait.LAYER);
        if (layer != null) {
            if (layers == null) {
                layers = new Stack<String>();
            }
            if (layers.empty() || !layers.peek().equals(layer)) {
                layers.push(layer);
                startLayer(layer);
                return true;
            }
        }
        return false;
    }

    protected void maybeEndLayer(Area area, boolean inNewLayer) {
        if (inNewLayer) {
            assert layers != null;
            assert !layers.empty();
            String layer = (String) area.getTrait(Trait.LAYER);
            assert layer != null;
            assert layers.peek().equals(layer);
            endLayer();
            layers.pop();
        }
    }

    /**
     * Renders a line area. <p>
     *
     * A line area may have grouped styling for its children such as underline,
     * background.</p>
     *
     * @param line  The line area
     */
    protected void renderLineArea(LineArea line) {
        List children = line.getInlineAreas();
        int saveBP = currentBPPosition;
        currentBPPosition += line.getSpaceBefore();
        int bl = line.getBidiLevel();
        if (bl >= 0) {
            if ((bl & 1) == 0) {
                currentIPPosition += line.getStartIndent();
            } else {
                currentIPPosition += line.getEndIndent();
            }
        } else {
            currentIPPosition += line.getStartIndent();
        }
        for (Object aChildren : children) {
            InlineArea inline = (InlineArea) aChildren;
            renderInlineArea(inline);
        }
        currentBPPosition = saveBP;
    }

    /**
     * Render the given InlineArea.
     * @param inlineArea inline area text to render
     */
    protected void renderInlineArea(InlineArea inlineArea) {
        List<ChangeBar> changeBarList = inlineArea.getChangeBarList();

        if (changeBarList != null && !changeBarList.isEmpty()) {
            drawChangeBars(inlineArea, changeBarList);
        }
        if (inlineArea instanceof TextArea) {
            renderText((TextArea) inlineArea);
        //} else if (inlineArea instanceof Character) {
            //renderCharacter((Character) inlineArea);
        } else if (inlineArea instanceof WordArea) {
            renderWord((WordArea) inlineArea);
        } else if (inlineArea instanceof SpaceArea) {
            renderSpace((SpaceArea) inlineArea);
        } else if (inlineArea instanceof InlineBlock) {
            renderInlineBlock((InlineBlock) inlineArea);
        } else if (inlineArea instanceof InlineParent) {
            renderInlineParent((InlineParent) inlineArea);
        } else if (inlineArea instanceof InlineBlockParent) {
            renderInlineBlockParent((InlineBlockParent) inlineArea);
        } else if (inlineArea instanceof Space) {
            renderInlineSpace((Space) inlineArea);
        } else if (inlineArea instanceof InlineViewport) {
            renderInlineViewport((InlineViewport) inlineArea);
        } else if (inlineArea instanceof Leader) {
            renderLeader((Leader) inlineArea);
        }
    }

    /**
     * Common method to render the background and borders for any inline area.
     * The all borders and padding are drawn outside the specified area.
     * @param area the inline area for which the background, border and padding is to be
     * rendered
     */
    protected abstract void renderInlineAreaBackAndBorders(InlineArea area);

    /**
     * Render the given Space.
     * @param space the space to render
     */
    protected void renderInlineSpace(Space space) {
        renderInlineAreaBackAndBorders(space);
        // an inline space moves the inline progression position
        // for the current block by the width or height of the space
        // it may also have styling (only on this object) that needs
        // handling
        currentIPPosition += space.getAllocIPD();
    }

    /**
     * Render the given Leader.
     * @param area the leader to render
     */
    protected void renderLeader(Leader area) {
        currentIPPosition += area.getAllocIPD();
    }

    /**
     * Render the given TextArea.
     * @param text the text to render
     */
    protected void renderText(TextArea text) {
        List children = text.getChildAreas();
        int saveIP = currentIPPosition;
        int saveBP = currentBPPosition;

        List<ChangeBar> changeBarList = text.getChangeBarList();

        if (changeBarList != null && !changeBarList.isEmpty()) {
            drawChangeBars(text, changeBarList);

            currentIPPosition = saveIP;
            currentBPPosition = saveBP;
        }

        for (Object aChildren : children) {
            InlineArea inline = (InlineArea) aChildren;
            renderInlineArea(inline);
        }
        currentIPPosition = saveIP + text.getAllocIPD();
    }

    /**
     * Render the given WordArea.
     * @param word the word to render
     */
    protected void renderWord(WordArea word) {
        currentIPPosition += word.getAllocIPD();
    }

    /**
     * Render the given SpaceArea.
     * @param space the space to render
     */
    protected void renderSpace(SpaceArea space) {
        currentIPPosition += space.getAllocIPD();
    }

    /**
     * Render the given InlineParent.
     * @param ip the inline parent to render
     */
    protected void renderInlineParent(InlineParent ip) {
        boolean inNewLayer = false;
        if (maybeStartLayer(ip)) {
            inNewLayer = true;
        }
        int level = ip.getBidiLevel();
        List children = ip.getChildAreas();
        renderInlineAreaBackAndBorders(ip);
        int saveIP = currentIPPosition;
        int saveBP = currentBPPosition;
        // if inline parent is a filled area (generated by Leader), and if
        // it is right-to-left, then adjust starting ip position in order to
        // align children to starting (right) edge of filled area
        int ipAdjust;
        if ((ip instanceof FilledArea) && ((level & 1) != 0)) {
            int ipdChildren = 0;
            for (Object aChildren : children) {
                InlineArea inline = (InlineArea) aChildren;
                ipdChildren += inline.getAllocIPD();
            }
            ipAdjust = ip.getAllocIPD() - ipdChildren;
        } else {
            ipAdjust = 0;
        }
        // perform inline position adjustments
        if ((level == -1) || ((level & 1) == 0)) {
            currentIPPosition += ip.getBorderAndPaddingWidthStart();
        } else {
            currentIPPosition += ip.getBorderAndPaddingWidthEnd();
            if (ipAdjust > 0) {
                currentIPPosition += ipAdjust;
            }
        }
        currentBPPosition += ip.getBlockProgressionOffset();
        // render children inlines
        for (Object aChildren : children) {
            InlineArea inline = (InlineArea) aChildren;
            renderInlineArea(inline);
        }
        currentIPPosition = saveIP + ip.getAllocIPD();
        currentBPPosition = saveBP;
        maybeEndLayer(ip, inNewLayer);
    }

    /**
     * Render the given InlineBlockParent.
     * @param ibp the inline block parent to render
     */
    protected void renderInlineBlockParent(InlineBlockParent ibp) {
        int level = ibp.getBidiLevel();
        renderInlineAreaBackAndBorders(ibp);
        if ((level == -1) || ((level & 1) == 0)) {
            currentIPPosition += ibp.getBorderAndPaddingWidthStart();
        } else {
            currentIPPosition += ibp.getBorderAndPaddingWidthEnd();
        }
        // For inline content the BP position is updated by the enclosing line area
        int saveBP = currentBPPosition;
        currentBPPosition += ibp.getBlockProgressionOffset();
        renderBlock(ibp.getChildArea());
        currentBPPosition = saveBP;
    }

    /**
     * Render the given Viewport.
     * @param viewport the viewport to render
     */
    protected void renderInlineViewport(InlineViewport viewport) {
        Area content = viewport.getContent();
        int saveBP = currentBPPosition;
        currentBPPosition += viewport.getBlockProgressionOffset();
        Rectangle2D contpos = viewport.getContentPosition();
        if (content instanceof Image) {
            renderImage((Image) content, contpos);
        } else if (content instanceof Container) {
            renderContainer((Container) content);
        } else if (content instanceof ForeignObject) {
            renderForeignObject((ForeignObject) content, contpos);
        } else if (content instanceof InlineBlockParent) {
            renderInlineBlockParent((InlineBlockParent) content);
        }
        currentIPPosition += viewport.getAllocIPD();
        currentBPPosition = saveBP;
    }

    /**
     * Renders an image area.
     *
     * @param image  The image
     * @param pos    The target position of the image
     * (todo) Make renderImage() protected
     */
    public void renderImage(Image image, Rectangle2D pos) {
        List<ChangeBar> changeBarList = image.getChangeBarList();

        if (changeBarList != null && !changeBarList.isEmpty()) {
            drawChangeBars(image, changeBarList);
        }
        // Default: do nothing.
        // Some renderers (ex. Text) don't support images.
    }

    /**
     * Tells the renderer to render an inline container.
     * @param cont  The inline container area
     */
    protected void renderContainer(Container cont) {
        int saveIP = currentIPPosition;
        int saveBP = currentBPPosition;

        List blocks = cont.getBlocks();
        renderBlocks(null, blocks);
        currentIPPosition = saveIP;
        currentBPPosition = saveBP;
    }

    /**
     * Renders a foreign object area.
     *
     * @param fo   The foreign object area
     * @param pos  The target position of the foreign object
     * (todo) Make renderForeignObject() protected
     */
    protected void renderForeignObject(ForeignObject fo, Rectangle2D pos) {
        List<ChangeBar> changeBarList = fo.getChangeBarList();

        if (changeBarList != null && !changeBarList.isEmpty()) {
            drawChangeBars(fo, changeBarList);
        }

        // Default: do nothing.
        // Some renderers (ex. Text) don't support foreign objects.
    }

    /**
     * Render the xml document with the given xml namespace.
     * The Render Context is by the handle to render into the current
     * rendering target.
     * @param ctx rendering context
     * @param doc DOM Document containing the source document
     * @param namespace Namespace URI of the document
     */
    public void renderXML(RendererContext ctx, Document doc,
                          String namespace) {
        XMLHandler handler = userAgent.getXMLHandlerRegistry().getXMLHandler(
                this, namespace);
        if (handler != null) {
            try {
                XMLHandlerConfigurator configurator
                    = new XMLHandlerConfigurator(userAgent);
                configurator.configure(ctx, namespace);
                handler.handleXML(ctx, doc, namespace);
            } catch (Exception e) {
                // could not handle document
                ResourceEventProducer eventProducer
                        = ResourceEventProducer.Provider.get(
                            ctx.getUserAgent().getEventBroadcaster());
                eventProducer.foreignXMLProcessingError(this, doc, namespace, e);
            }
        } else {
            if (warnedXMLHandlers == null) {
                warnedXMLHandlers = new java.util.HashSet();
            }
            if (!warnedXMLHandlers.contains(namespace)) {
                // no handler found for document
                warnedXMLHandlers.add(namespace);
                ResourceEventProducer eventProducer = ResourceEventProducer.Provider.get(
                        ctx.getUserAgent().getEventBroadcaster());
                eventProducer.foreignXMLNoHandler(this, doc, namespace);
            }
        }
    }

    /**
     * Converts a millipoint-based transformation matrix to points.
     * @param at a millipoint-based transformation matrix
     * @return a point-based transformation matrix
     */
    protected AffineTransform mptToPt(AffineTransform at) {
        double[] matrix = new double[6];
        at.getMatrix(matrix);
        //Convert to points
        matrix[4] = matrix[4] / 1000;
        matrix[5] = matrix[5] / 1000;
        return new AffineTransform(matrix);
    }

    /**
     * Converts a point-based transformation matrix to millipoints.
     * @param at a point-based transformation matrix
     * @return a millipoint-based transformation matrix
     */
    protected AffineTransform ptToMpt(AffineTransform at) {
        double[] matrix = new double[6];
        at.getMatrix(matrix);
        //Convert to millipoints
        //Math.round() because things like this can happen: 65.6 * 1000 = 65.599999999999999
        //which is bad for testing
        matrix[4] = Math.round(matrix[4] * 1000);
        matrix[5] = Math.round(matrix[5] * 1000);
        return new AffineTransform(matrix);
    }

    /**
     * Draws all change bars associated with an area.
     *
     * @param area The area to draw change bars for
     * @param changeBarList The list of change bars affecting the area
     */
    protected void drawChangeBars(Area area, List<ChangeBar> changeBarList) {

        if (area.getTraitAsBoolean(Trait.IS_REFERENCE_AREA)) {
            return;
        }

        Block changeBarArea;

        int saveIP = currentIPPosition;
        int saveBP = currentBPPosition;

        int currentColumnStartIP = columnStartIPPosition;
        int currentColumnEndIP = columnEndIPPosition;
        int currentColumnLeftIP = columnLeftIPPosition;
        int currentColumnRightIP = columnRightIPPosition;

        for (ChangeBar changeBar : changeBarList) {

            boolean isLeftToRight = (inlineProgressionDirection == null)
                    || (inlineProgressionDirection.getEnumValue() == Constants.EN_LR);

            changeBarArea = new Block();

            // currentIPPosition is reset to zero so from now on all multicolumn
            // dimensions has to be calculated relatively to the given column
            currentIPPosition = 0;
            currentBPPosition = saveBP;

            int changeBarWidth = changeBar.getWidth().getValue();
            int changeBarOffset = changeBar.getOffset().getValue();

            if (isLeftToRight) {
                currentColumnStartIP = columnStartIPPosition - changeBarWidth;
                currentColumnLeftIP = columnLeftIPPosition - changeBarWidth;
            } else {
                currentColumnEndIP = columnEndIPPosition - changeBarWidth;
                currentColumnLeftIP = columnLeftIPPosition - changeBarWidth;
            }

            // xOffset by default is negative width for change bars placed on the
            // start edge (overriden if placement is at the end edge)
            int xOffset = currentColumnStartIP;

            // xScale is for adding or subtracting the offset of the change bar
            // depending on placing the bar towards or away from the edge it is
            // bound to
            int xScale = -1;

            // determines currentIPPosition based on placement
            switch (changeBar.getPlacement()) {
                case EN_START:
                    xOffset = currentColumnStartIP;
                    xScale = -1;
                    break;
                case EN_END:
                    xOffset = currentColumnEndIP;
                    xScale = 1;
                    break;
                case EN_LEFT:
                    xOffset = currentColumnLeftIP;
                    xScale = (isLeftToRight) ? -1 : 1;
                    break;
                case EN_RIGHT:
                    xOffset = currentColumnRightIP;
                    xScale = (isLeftToRight) ? 1 : -1;
                    break;
                case EN_INSIDE:
                    if (bindingOnStartEdge) {
                        xOffset = currentColumnStartIP;
                        xScale = -1;
                    } else if (bindingOnEndEdge) {
                        xOffset = currentColumnEndIP;
                        xScale = 1;
                    } else {
                        xOffset = currentColumnStartIP;
                        xScale = -1;
                    }
                    break;
                case EN_OUTSIDE:
                    if (bindingOnStartEdge) {
                        xOffset = columnEndIPPosition;
                        xScale = 1;
                    } else if (bindingOnEndEdge) {
                        xOffset = columnStartIPPosition;
                        xScale = -1;
                    } else {
                        xOffset = columnStartIPPosition;
                        xScale = -1;
                    }
                    break;
                case EN_ALTERNATE:
                    if (columnCount == 2) {
                        if (columnIndex == 0) {
                            xOffset = columnStartIPPosition;
                            xScale = -1;
                        } else {
                            xOffset = columnEndIPPosition;
                            xScale = 1;
                        }
                    } else {
                        if (bindingOnStartEdge) {
                            xOffset = columnEndIPPosition;
                            xScale = 1;
                        } else if (bindingOnEndEdge) {
                            xOffset = columnStartIPPosition;
                            xScale = -1;
                        } else {
                            xOffset = columnStartIPPosition;
                            xScale = -1;
                        }
                    }
                    break;
                default:
                    break;
            }

            if (isLeftToRight) {
                xOffset += xScale * changeBarOffset;
            } else {
                xOffset -= xScale * changeBarOffset;
            }

            xOffset += getBeginOffset();

            // Change bar area has 0 ipd, class xsl-absolute, no margin or padding
            changeBarArea.setAreaClass(Area.CLASS_ABSOLUTE);
            changeBarArea.setIPD(0);

            BorderProps props = BorderProps.makeRectangular(
                    changeBar.getStyle(), changeBarWidth, changeBar.getColor(),
                    BorderProps.Mode.SEPARATE);

            changeBarArea.addTrait(Trait.BORDER_START, props);
            changeBarArea.addTrait(Trait.BORDER_END, props);

            changeBarArea.setXOffset(xOffset);

            int areaHeight = area.getAllocBPD();

            if (area instanceof BlockParent) {
                changeBarArea.setBPD(areaHeight);
                changeBarArea.setYOffset(((BlockParent) area).getYOffset());
                renderBlock(changeBarArea);

            } else {
                if (areaHeight > 0) {
                    Property p = changeBar.getLineHeight().getOptimum(DummyPercentBaseContext.getInstance());
                    int lineHeight = p.getLength().getValue();
                    changeBarArea.setBPD(lineHeight);
                    changeBarArea.setYOffset(areaHeight - lineHeight);
                }
                renderInlineBlock(new InlineBlock(changeBarArea));
            }

            // restore position on page
            currentIPPosition = saveIP;
            currentBPPosition = saveBP;
        }
    }

    /**
     * Returns the begin offset of the inline begin (changes by reference area
     * transforms).
     *
     * @return the offset from current coordinate system 0 that the IP begin is
     * at
     */
    protected int getBeginOffset() {
        return beginOffset;
    }

    /**
     * Sets the begin offset for inline progression begin (changes by reference
     * area tranforms).
     *
     * @param offset the new offset from IPP 0 that true IP start is at
     */
    protected void setBeginOffset(int offset) {
        beginOffset = offset;
    }
}
