/*
 * 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: AFPPainter.java 1884462 2020-12-15 13:28:47Z ssteiner $ */

package org.apache.fop.render.afp;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.util.Map;

import org.w3c.dom.Document;

import org.apache.xmlgraphics.image.loader.Image;
import org.apache.xmlgraphics.image.loader.ImageException;
import org.apache.xmlgraphics.image.loader.ImageInfo;
import org.apache.xmlgraphics.image.loader.ImageProcessingHints;
import org.apache.xmlgraphics.image.loader.ImageSessionContext;
import org.apache.xmlgraphics.image.loader.ImageSize;
import org.apache.xmlgraphics.image.loader.impl.ImageGraphics2D;
import org.apache.xmlgraphics.java2d.Graphics2DImagePainter;

import org.apache.fop.afp.AFPBorderPainter;
import org.apache.fop.afp.AFPEventProducer;
import org.apache.fop.afp.AFPObjectAreaInfo;
import org.apache.fop.afp.AFPPaintingState;
import org.apache.fop.afp.AFPResourceInfo;
import org.apache.fop.afp.AFPUnitConverter;
import org.apache.fop.afp.AbstractAFPPainter;
import org.apache.fop.afp.BorderPaintingInfo;
import org.apache.fop.afp.DataStream;
import org.apache.fop.afp.RectanglePaintingInfo;
import org.apache.fop.afp.fonts.AFPFont;
import org.apache.fop.afp.fonts.AFPFontAttributes;
import org.apache.fop.afp.fonts.AFPPageFonts;
import org.apache.fop.afp.fonts.CharacterSet;
import org.apache.fop.afp.modca.AbstractPageObject;
import org.apache.fop.afp.modca.PresentationTextObject;
import org.apache.fop.afp.ptoca.PtocaBuilder;
import org.apache.fop.afp.ptoca.PtocaProducer;
import org.apache.fop.afp.util.AFPResourceAccessor;
import org.apache.fop.fonts.Font;
import org.apache.fop.fonts.FontTriplet;
import org.apache.fop.fonts.FontType;
import org.apache.fop.fonts.Typeface;
import org.apache.fop.render.ImageHandlerUtil;
import org.apache.fop.render.RenderingContext;
import org.apache.fop.render.intermediate.AbstractIFPainter;
import org.apache.fop.render.intermediate.BorderPainter;
import org.apache.fop.render.intermediate.GraphicsPainter;
import org.apache.fop.render.intermediate.IFException;
import org.apache.fop.render.intermediate.IFState;
import org.apache.fop.render.intermediate.IFUtil;
import org.apache.fop.traits.BorderProps;
import org.apache.fop.traits.RuleStyle;
import org.apache.fop.util.CharUtilities;

/**
 * IFPainter implementation that produces AFP (MO:DCA).
 */
public class AFPPainter extends AbstractIFPainter<AFPDocumentHandler> {

    private static final int X = 0;

    private static final int Y = 1;

    private final GraphicsPainter graphicsPainter;

    /** the border painter */
    private final AFPBorderPainterAdapter borderPainter;
    /** the rectangle painter */
    private final AbstractAFPPainter rectanglePainter;

    /** unit converter */
    private final AFPUnitConverter unitConv;

    private final AFPEventProducer eventProducer;
    private Integer bytesAvailable;

    /**
     * Default constructor.
     * @param documentHandler the parent document handler
     */
    public AFPPainter(AFPDocumentHandler documentHandler) {
        super(documentHandler);
        this.state = IFState.create();
        this.graphicsPainter = new AFPGraphicsPainter(
                new AFPBorderPainter(getPaintingState(), getDataStream()));
        this.borderPainter = new AFPBorderPainterAdapter(graphicsPainter, this, documentHandler);
        this.rectanglePainter = documentHandler.createRectanglePainter();
        this.unitConv = getPaintingState().getUnitConverter();
        this.eventProducer = AFPEventProducer.Provider.get(getUserAgent().getEventBroadcaster());
    }

    private AFPPaintingState getPaintingState() {
        return getDocumentHandler().getPaintingState();
    }

    private DataStream getDataStream() {
        return getDocumentHandler().getDataStream();
    }

    @Override
    public String getFontKey(FontTriplet triplet) throws IFException {
        try {
            return super.getFontKey(triplet);
        } catch (IFException e) {
            eventProducer.invalidConfiguration(null, e);
            return super.getFontKey(FontTriplet.DEFAULT_FONT_TRIPLET);
        }
    }

    /** {@inheritDoc} */
    public void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect)
            throws IFException {
        //AFP doesn't support clipping, so we treat viewport like a group
        //this is the same code as for startGroup()
        try {
            saveGraphicsState();
            concatenateTransformationMatrix(transform);
        } catch (IOException ioe) {
            throw new IFException("I/O error in startViewport()", ioe);
        }
    }

    /** {@inheritDoc} */
    public void endViewport() throws IFException {
        try {
            restoreGraphicsState();
        } catch (IOException ioe) {
            throw new IFException("I/O error in endViewport()", ioe);
        }
    }

    private void concatenateTransformationMatrix(AffineTransform at) {
        if (!at.isIdentity()) {
            getPaintingState().concatenate(at);
        }
    }

    /** {@inheritDoc} */
    public void startGroup(AffineTransform transform, String layer) throws IFException {
        try {
            saveGraphicsState();
            concatenateTransformationMatrix(transform);
        } catch (IOException ioe) {
            throw new IFException("I/O error in startGroup()", ioe);
        }
    }

    /** {@inheritDoc} */
    public void endGroup() throws IFException {
        try {
            restoreGraphicsState();
        } catch (IOException ioe) {
            throw new IFException("I/O error in endGroup()", ioe);
        }
    }

    /** {@inheritDoc} */
    @Override
    protected Map createDefaultImageProcessingHints(ImageSessionContext sessionContext) {
        Map hints = super.createDefaultImageProcessingHints(sessionContext);

        //AFP doesn't support alpha channels
        hints.put(ImageProcessingHints.TRANSPARENCY_INTENT,
                ImageProcessingHints.TRANSPARENCY_INTENT_IGNORE);
        hints.put("CMYK", getDocumentHandler().getPaintingState().isCMYKImagesSupported());
        return hints;
    }

    /** {@inheritDoc} */
    @Override
    protected RenderingContext createRenderingContext() {
        AFPRenderingContext renderingContext = new AFPRenderingContext(
                getUserAgent(),
                getDocumentHandler().getResourceManager(),
                getPaintingState(),
                getFontInfo(),
                getContext().getForeignAttributes());
        return renderingContext;
    }

    /** {@inheritDoc} */
    public void drawImage(String uri, Rectangle rect) throws IFException {
        PageSegmentDescriptor pageSegment = getDocumentHandler().getPageSegmentNameFor(uri);

        if (pageSegment != null) {
            float[] srcPts = {rect.x, rect.y};
            int[] coords = unitConv.mpts2units(srcPts);
            int width = Math.round(unitConv.mpt2units(rect.width));
            int height = Math.round(unitConv.mpt2units(rect.height));

            getDataStream().createIncludePageSegment(pageSegment.getName(),
                    coords[X], coords[Y], width, height);

            //Do we need to embed an external page segment?
            if (pageSegment.getURI() != null) {
                AFPResourceAccessor accessor = new AFPResourceAccessor(
                        getDocumentHandler().getUserAgent().getResourceResolver());
                try {
                    URI resourceUri = new URI(pageSegment.getURI());
                    getDocumentHandler().getResourceManager().createIncludedResourceFromExternal(
                            pageSegment.getName(), resourceUri, accessor);

                } catch (URISyntaxException urie) {
                    throw new IFException("Could not handle resource url"
                            + pageSegment.getURI(), urie);
                } catch (IOException ioe) {
                    throw new IFException("Could not handle resource" + pageSegment.getURI(), ioe);
                }
            }

        } else {
            drawImageUsingURI(uri, rect);
        }
    }

    /** {@inheritDoc} */
    protected void drawImage(Image image, Rectangle rect,
            RenderingContext context, boolean convert, Map additionalHints)
                    throws IOException, ImageException {


        AFPRenderingContext afpContext = (AFPRenderingContext) context;

        AFPResourceInfo resourceInfo = AFPImageHandler.createResourceInformation(
                image.getInfo().getOriginalURI(),
                afpContext.getForeignAttributes());

        //Check if the image is cached before processing it again
        if (afpContext.getResourceManager().isObjectCached(resourceInfo)) {

            AFPObjectAreaInfo areaInfo = AFPImageHandler.createObjectAreaInfo(
                    afpContext.getPaintingState(), rect);

            afpContext.getResourceManager().includeCachedObject(resourceInfo, areaInfo);

        } else {
            super.drawImage(image, rect, context, convert, additionalHints);
        }

    }

    /** {@inheritDoc} */
    public void drawImage(Document doc, Rectangle rect) throws IFException {
        drawImageUsingDocument(doc, rect);
    }

    /** {@inheritDoc} */
    public void clipRect(Rectangle rect) throws IFException {
        //Not supported!
    }

    private float toPoint(int mpt) {
        return mpt / 1000f;
    }

    /** {@inheritDoc} */
    public void fillRect(Rectangle rect, Paint fill) throws IFException {
        if (fill == null) {
            return;
        }
        if (rect.width != 0 && rect.height != 0) {
            if (fill instanceof Color) {
                getPaintingState().setColor((Color) fill);
            } else {
                throw new UnsupportedOperationException("Non-Color paints NYI");
            }
            RectanglePaintingInfo rectanglePaintInfo = new RectanglePaintingInfo(
                    toPoint(rect.x), toPoint(rect.y), toPoint(rect.width), toPoint(rect.height));
            try {
                rectanglePainter.paint(rectanglePaintInfo);
            } catch (IOException ioe) {
                throw new IFException("IO error while painting rectangle", ioe);
            }
        }
    }

    @Override
    public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom,
            BorderProps left, BorderProps right, Color innerBackgroundColor) throws IFException {
        if (top != null || bottom != null || left != null || right != null) {
            this.borderPainter.drawBorders(rect, top, bottom, left, right, innerBackgroundColor);
        }
    }


    private static final class AFPGraphicsPainter implements GraphicsPainter {

        private final AFPBorderPainter graphicsPainter;

        private AFPGraphicsPainter(AFPBorderPainter delegate) {
            this.graphicsPainter = delegate;
        }

        public void drawBorderLine(int x1, int y1, int x2, int y2,
                boolean horz, boolean startOrBefore, int style, Color color)
                        throws IOException {
            BorderPaintingInfo borderPaintInfo = new BorderPaintingInfo(
                    toPoints(x1), toPoints(y1), toPoints(x2), toPoints(y2),
                    horz, style, color);
            graphicsPainter.paint(borderPaintInfo);
        }

        private float toPoints(int mpt) {
            return mpt / 1000f;
        }

        public void drawLine(Point start, Point end, int width,
                Color color, RuleStyle style) throws IOException {
            if (start.y != end.y) {
                //TODO Support arbitrary lines if necessary
                throw new UnsupportedOperationException("Can only deal with horizontal lines right now");
            }
            //Simply delegates to drawBorderLine() as AFP line painting is not very sophisticated.
            int halfWidth = width / 2;
            drawBorderLine(start.x, start.y - halfWidth, end.x, start.y + halfWidth,
                    true, true, style.getEnumValue(), color);
        }

        public void moveTo(int x, int y) throws IOException {
        }

        public void lineTo(int x, int y) throws IOException {
        }

        public void arcTo(double startAngle, double endAngle, int cx, int cy,
                int width, int height) throws IOException {
        }

        public void rotateCoordinates(double angle) throws IOException {
            throw new UnsupportedOperationException("Cannot handle coordinate rotation");
        }

        public void translateCoordinates(int xTranslate, int yTranslate) throws IOException {
            throw new UnsupportedOperationException("Cannot handle coordinate translation");
        }

        public void scaleCoordinates(float xScale, float yScale) throws IOException {
            throw new UnsupportedOperationException("Cannot handle coordinate scaling");
        }

        public void closePath() throws IOException {
        }

        public void clip() throws IOException {
        }

        public void saveGraphicsState() throws IOException {
        }

        public void restoreGraphicsState() throws IOException {
        }


    }

    //TODO Try to resolve the name-clash between the AFPBorderPainter in the afp package
    //and this one. Not done for now to avoid a lot of re-implementation and code duplication.
    private static class AFPBorderPainterAdapter extends BorderPainter {

        private final class BorderImagePainter implements Graphics2DImagePainter {
            private final double cornerCorrectionFactor;
            private final Rectangle borderRect;
            private final BorderProps bpsStart;
            private final BorderProps bpsEnd;
            private final BorderProps bpsBefore;
            private final BorderProps bpsAfter;
            private final boolean[] roundCorner;
            private final Color innerBackgroundColor;

            /* TODO represent border related parameters in a class */
            private BorderImagePainter(double cornerCorrectionFactor, Rectangle borderRect,
                    BorderProps bpsStart, BorderProps bpsEnd,
                    BorderProps bpsBefore, BorderProps bpsAfter,
                    boolean[] roundCorner, Color innerBackgroundColor) {
                this.cornerCorrectionFactor = cornerCorrectionFactor;
                this.borderRect = borderRect;
                this.bpsStart = bpsStart;
                this.bpsBefore = bpsBefore;
                this.roundCorner = roundCorner;
                this.bpsEnd = bpsEnd;
                this.bpsAfter = bpsAfter;
                this.innerBackgroundColor = innerBackgroundColor;
            }

            public void paint(Graphics2D g2d, Rectangle2D area) {

                //background
                Area background = new Area(area);
                Area cornerRegion = new Area();
                Area[] cornerBorder = new Area[]{new Area(), new Area(), new Area(), new Area()};
                Area[] clip = new Area[4];
                if (roundCorner[TOP_LEFT]) {
                    AffineTransform transform =  new AffineTransform();
                    int beforeRadius = (int)(cornerCorrectionFactor * bpsBefore.getRadiusStart());
                    int startRadius = (int)(cornerCorrectionFactor * bpsStart.getRadiusStart());

                    int beforeWidth = bpsBefore.width;
                    int startWidth = bpsStart.width;
                    int corner = TOP_LEFT;

                    background.subtract(makeCornerClip(beforeRadius, startRadius,
                            transform));

                    clip[TOP_LEFT] = new Area(new Rectangle(0, 0, startRadius, beforeRadius));
                    clip[TOP_LEFT].transform(transform);
                    cornerRegion.add(clip[TOP_LEFT]);

                    cornerBorder[TOP].add(makeCornerBorderBPD(beforeRadius,
                                    startRadius, beforeWidth, startWidth, transform));

                    cornerBorder[LEFT].add(makeCornerBorderIPD(beforeRadius,
                            startRadius, beforeWidth, startWidth, transform));
                }

                if (roundCorner[TOP_RIGHT]) {
                    AffineTransform transform
                            = new AffineTransform(-1, 0, 0, 1, borderRect.width, 0);

                    int beforeRadius = (int)(cornerCorrectionFactor * bpsBefore.getRadiusEnd());
                    int startRadius = (int)(cornerCorrectionFactor * bpsEnd.getRadiusStart());

                    int beforeWidth = bpsBefore.width;
                    int startWidth = bpsEnd.width;
                    int corner = TOP_RIGHT;

                    background.subtract(makeCornerClip(beforeRadius, startRadius,
                            transform));

                    clip[TOP_RIGHT] = new Area(new Rectangle(0, 0, startRadius, beforeRadius));
                    clip[TOP_RIGHT].transform(transform);
                    cornerRegion.add(clip[TOP_RIGHT]);

                    cornerBorder[TOP].add(makeCornerBorderBPD(beforeRadius,
                                    startRadius, beforeWidth, startWidth, transform));

                    cornerBorder[RIGHT].add(makeCornerBorderIPD(beforeRadius,
                            startRadius, beforeWidth, startWidth, transform));
                }

                if (roundCorner[BOTTOM_RIGHT]) {
                    AffineTransform transform = new AffineTransform(-1, 0, 0, -1,
                            borderRect.width, borderRect.height);

                    int beforeRadius = (int)(cornerCorrectionFactor * bpsAfter.getRadiusEnd());
                    int startRadius = (int)(cornerCorrectionFactor * bpsEnd.getRadiusEnd());

                    int beforeWidth = bpsAfter.width;
                    int startWidth = bpsEnd.width;
                    int corner = BOTTOM_RIGHT;

                    background.subtract(makeCornerClip(beforeRadius, startRadius,
                            transform));

                    clip[BOTTOM_RIGHT] = new Area(new Rectangle(0, 0, startRadius, beforeRadius));
                    clip[BOTTOM_RIGHT].transform(transform);
                    cornerRegion.add(clip[BOTTOM_RIGHT]);

                    cornerBorder[BOTTOM].add(makeCornerBorderBPD(beforeRadius,
                            startRadius, beforeWidth, startWidth, transform));
                    cornerBorder[RIGHT].add(makeCornerBorderIPD(beforeRadius,
                            startRadius, beforeWidth, startWidth, transform));
                }

                if (roundCorner[BOTTOM_LEFT]) {
                    AffineTransform transform
                            = new AffineTransform(1, 0, 0, -1, 0, borderRect.height);

                    int beforeRadius = (int)(cornerCorrectionFactor * bpsAfter.getRadiusStart());
                    int startRadius = (int)(cornerCorrectionFactor * bpsStart.getRadiusEnd());

                    int beforeWidth = bpsAfter.width;
                    int startWidth = bpsStart.width;
                    int corner = BOTTOM_LEFT;

                    background.subtract(makeCornerClip(beforeRadius, startRadius,
                            transform));

                    clip[BOTTOM_LEFT] = new Area(new Rectangle(0, 0, startRadius, beforeRadius));
                    clip[BOTTOM_LEFT].transform(transform);
                    cornerRegion.add(clip[BOTTOM_LEFT]);

                    cornerBorder[BOTTOM].add(makeCornerBorderBPD(beforeRadius,
                                    startRadius, beforeWidth, startWidth, transform));
                    cornerBorder[LEFT].add(makeCornerBorderIPD(beforeRadius,
                            startRadius, beforeWidth, startWidth, transform));
                }

                g2d.setColor(innerBackgroundColor);
                g2d.fill(background);

                //paint the borders
                //TODO refactor to repeating code into method
                if (bpsBefore != null && bpsBefore.width > 0) {
                    GeneralPath borderPath = new GeneralPath();
                    borderPath.moveTo(0, 0);
                    borderPath.lineTo(borderRect.width, 0);
                    borderPath.lineTo(
                            borderRect.width - (bpsEnd == null ? 0 : bpsEnd.width),
                            bpsBefore.width);
                    borderPath.lineTo(bpsStart == null ? 0 : bpsStart.width, bpsBefore.width);

                    Area border = new Area(borderPath);

                    if (clip[TOP_LEFT] != null) {
                        border.subtract(clip[TOP_LEFT]);
                    }
                    if (clip[TOP_RIGHT] != null) {
                        border.subtract(clip[TOP_RIGHT]);
                    }

                    g2d.setColor(bpsBefore.color);
                    g2d.fill(border);
                    g2d.fill(cornerBorder[TOP]);
                }

                if (bpsEnd != null && bpsEnd.width > 0) {
                    GeneralPath borderPath = new GeneralPath();
                    borderPath.moveTo(borderRect.width, 0);
                    borderPath.lineTo(borderRect.width, borderRect.height);
                    borderPath.lineTo(
                            borderRect.width - bpsEnd.width,
                            borderRect.height - (bpsAfter == null ? 0 : bpsAfter.width));
                    borderPath.lineTo(
                            borderRect.width - bpsEnd.width,
                            bpsBefore == null ? 0 : bpsBefore.width);

                    Area border = new Area(borderPath);

                    if (clip[BOTTOM_RIGHT] != null) {
                        border.subtract(clip[BOTTOM_RIGHT]);
                    }
                    if (clip[TOP_RIGHT] != null) {
                        border.subtract(clip[TOP_RIGHT]);
                    }

                    g2d.setColor(bpsEnd.color);
                    g2d.fill(border);
                    g2d.fill(cornerBorder[RIGHT]);
                }

                if (bpsAfter != null && bpsAfter.width > 0) {
                    GeneralPath borderPath = new GeneralPath();
                    borderPath.moveTo(0, borderRect.height);
                    borderPath.lineTo(borderRect.width, borderRect.height);
                    borderPath.lineTo(
                            borderRect.width - (bpsEnd == null ? 0 : bpsEnd.width),
                            borderRect.height - bpsAfter.width);
                    borderPath.lineTo(bpsStart == null ? 0 : bpsStart.width,
                            borderRect.height - bpsAfter.width);
                    Area border = new Area(borderPath);
                    if (clip[BOTTOM_LEFT] != null) {
                        border.subtract(clip[BOTTOM_LEFT]);
                    }
                    if (clip[BOTTOM_RIGHT] != null) {
                        border.subtract(clip[BOTTOM_RIGHT]);
                    }
                    g2d.setColor(bpsAfter.color);
                    g2d.fill(border);
                    g2d.fill(cornerBorder[BOTTOM]);
                }

                if (bpsStart != null && bpsStart.width > 0) {

                    GeneralPath borderPath = new GeneralPath();
                    borderPath.moveTo(bpsStart.width,
                            bpsBefore == null ? 0 : bpsBefore.width);
                    borderPath.lineTo(bpsStart.width,
                            borderRect.height - (bpsAfter == null ? 0 : bpsAfter.width));
                    borderPath.lineTo(0, borderRect.height);
                    borderPath.lineTo(0, 0);

                    Area border = new Area(borderPath);

                    if (clip[BOTTOM_LEFT] != null) {
                        border.subtract(clip[BOTTOM_LEFT]);
                    }
                    if (clip[TOP_LEFT] != null) {
                        border.subtract(clip[TOP_LEFT]);
                    }
                    g2d.setColor(bpsStart.color);
                    g2d.fill(border);
                    g2d.fill(cornerBorder[LEFT]);
                }
            }

            public Dimension getImageSize() {
                return borderRect.getSize();
            }
        }

        private final AFPPainter painter;
        private final AFPDocumentHandler documentHandler;

        public AFPBorderPainterAdapter(GraphicsPainter graphicsPainter, AFPPainter painter,
                AFPDocumentHandler documentHandler) {
            super(graphicsPainter);
            this.painter = painter;
            this.documentHandler = documentHandler;
        }

        public void drawBorders(final Rectangle borderRect,
                final BorderProps bpsBefore, final BorderProps bpsAfter,
                final BorderProps bpsStart, final BorderProps bpsEnd, Color innerBackgroundColor)
                        throws IFException {
            drawRoundedCorners(borderRect, bpsBefore, bpsAfter, bpsStart, bpsEnd, innerBackgroundColor);
        }

        private boolean isBackgroundRequired(BorderProps bpsBefore, BorderProps bpsAfter,
                BorderProps bpsStart, BorderProps bpsEnd) {
            return !hasRoundedCorners(bpsBefore,  bpsAfter, bpsStart,  bpsEnd);
        }

        private boolean hasRoundedCorners(final BorderProps bpsBefore, final BorderProps bpsAfter,
                final BorderProps bpsStart, final BorderProps bpsEnd) {
            return ((bpsStart == null ? false : bpsStart.getRadiusStart() > 0)
                    && (bpsBefore == null ? false : bpsBefore.getRadiusStart() > 0))
                    || ((bpsBefore == null ? false : bpsBefore.getRadiusEnd() > 0)
                            && (bpsEnd == null ? false : bpsEnd.getRadiusStart() > 0))
                            || ((bpsEnd == null ? false : bpsEnd.getRadiusEnd() > 0)
                                    && (bpsAfter == null ? false : bpsAfter.getRadiusEnd() > 0))
                                    || ((bpsAfter == null ? false : bpsAfter.getRadiusStart() > 0)
                                            && (bpsStart == null ? false : bpsStart.getRadiusEnd() > 0));
        }

        private void drawRoundedCorners(final Rectangle borderRect,
                final BorderProps bpsBefore, final BorderProps bpsAfter,
                final BorderProps bpsStart, final BorderProps bpsEnd,
                final Color innerBackgroundColor) throws IFException {
            final double cornerCorrectionFactor = calculateCornerCorrectionFactor(borderRect.width,
                    borderRect.height, bpsBefore,  bpsAfter, bpsStart,  bpsEnd);
            final boolean[] roundCorner = new boolean[]{
                    bpsBefore != null  && bpsStart != null
                            && bpsBefore.getRadiusStart() > 0
                            && bpsStart.getRadiusStart() > 0
                            && isNotCollapseOuter(bpsBefore)
                            && isNotCollapseOuter(bpsStart),
                            bpsEnd != null && bpsBefore != null
                            && bpsEnd.getRadiusStart() > 0
                            && bpsBefore.getRadiusEnd() > 0
                            && isNotCollapseOuter(bpsEnd)
                            && isNotCollapseOuter(bpsBefore),
                            bpsEnd != null && bpsAfter != null
                            && bpsEnd.getRadiusEnd() > 0
                            && bpsAfter.getRadiusEnd() > 0
                            && isNotCollapseOuter(bpsEnd)
                            && isNotCollapseOuter(bpsAfter),
                            bpsStart != null && bpsAfter != null
                            && bpsStart.getRadiusEnd() > 0
                            && bpsAfter.getRadiusStart() > 0
                            && isNotCollapseOuter(bpsStart)
                            && isNotCollapseOuter(bpsAfter)
            };

            if (!roundCorner[TOP_LEFT] && !roundCorner[TOP_RIGHT]
                    && !roundCorner[BOTTOM_RIGHT] && !roundCorner[BOTTOM_LEFT]) {
                try {
                    drawRectangularBorders(borderRect, bpsBefore, bpsAfter, bpsStart, bpsEnd);
                } catch (IOException ioe) {
                    throw new IFException("IO error drawing borders", ioe);
                }
                return;
            }

            String areaKey = makeKey(borderRect,
                    bpsBefore, bpsEnd, bpsAfter,
                    bpsStart, innerBackgroundColor);

            Graphics2DImagePainter painter = null;
            String name = documentHandler.getCachedRoundedCorner(areaKey);

            if (name == null) {

                name = documentHandler.cacheRoundedCorner(areaKey);

                painter = new BorderImagePainter(cornerCorrectionFactor, borderRect,
                        bpsStart, bpsEnd, bpsBefore, bpsAfter,
                        roundCorner, innerBackgroundColor);
            }
            paintCornersAsBitmap(painter, borderRect, name);
        }

        private boolean isNotCollapseOuter(BorderProps bp) {
            return !bp.isCollapseOuter();
        }

        private Area makeCornerClip(final int beforeRadius, final int startRadius,
                final AffineTransform transform) {

            Rectangle clipR = new Rectangle(0, 0, startRadius, beforeRadius);

            Area clip = new Area(clipR);

            Ellipse2D.Double e = new  Ellipse2D.Double();
            e.x = 0;
            e.y = 0;
            e.width = 2 * startRadius;
            e.height = 2 * beforeRadius;

            clip.subtract(new Area(e));

            clip.transform(transform);
            return clip;
        }


        private Area makeCornerBorderBPD(final int beforeRadius, final int startRadius,
                final int beforeWidth, final int startWidth, final AffineTransform transform) {

            Rectangle clipR = new Rectangle(0, 0, startRadius, beforeRadius);

            Ellipse2D.Double e = new  Ellipse2D.Double();
            e.x = 0;
            e.y = 0;
            e.width = 2 * startRadius;
            e.height = 2 * beforeRadius;

            Ellipse2D.Double i = new  Ellipse2D.Double();
            i.x = startWidth;
            i.y = beforeWidth;
            i.width = 2 * (startRadius - startWidth);
            i.height = 2 * (beforeRadius - beforeWidth);

            Area clip = new Area(e);
            clip.subtract(new Area(i));
            clip.intersect(new Area(clipR));

            GeneralPath cut = new GeneralPath();
            cut.moveTo(0, 0);
            cut.lineTo(startRadius, ((float) startRadius * beforeWidth) / startWidth);
            cut.lineTo(startRadius, 0);
            clip.intersect(new Area(cut));
            clip.transform(transform);
            return clip;
        }


        private Area makeCornerBorderIPD(final int beforeRadius, final int startRadius,
                final int beforeWidth, final int startWidth, final AffineTransform transform) {

            Rectangle clipR = new Rectangle(0, 0, startRadius, beforeRadius);


            Ellipse2D.Double e = new  Ellipse2D.Double();
            e.x = 0;
            e.y = 0;
            e.width = 2 * startRadius;
            e.height = 2 * beforeRadius;

            Ellipse2D.Double i = new  Ellipse2D.Double();
            i.x = startWidth;
            i.y = beforeWidth;
            i.width = 2 * (startRadius - startWidth);
            i.height = 2 * (beforeRadius - beforeWidth);

            Area clip = new Area(e);
            clip.subtract(new Area(i));
            clip.intersect(new Area(clipR));

            GeneralPath cut = new GeneralPath();
            cut.moveTo(0, 0);
            cut.lineTo(startRadius, ((float) startRadius * beforeWidth) / startWidth);
            cut.lineTo(startRadius, 0);
            clip.subtract(new Area(cut));
            clip.transform(transform);
            return clip;
        }

        private String makeKey(Rectangle area, BorderProps beforeProps,
                BorderProps endProps, BorderProps afterProps, BorderProps startProps,
                Color innerBackgroundColor) {

            return hash(new StringBuffer()
                    .append(area.width)
                    .append(":")
                    .append(area.height)
                    .append(":")
                    .append(beforeProps)
                    .append(":")
                    .append(endProps)
                    .append(":")
                    .append(afterProps)
                    .append(":")
                    .append(startProps)
                    .append(":")
                    .append(innerBackgroundColor)
                    .toString());
        }


        private String hash(String text) {

            MessageDigest md;
            try {
                md = MessageDigest.getInstance("MD5");
            } catch (Exception e) {
                throw new RuntimeException("Internal error", e);
            }

            byte[] result = md.digest(text.getBytes());

            StringBuffer sb = new StringBuffer();
            char[] digits = {'0', '1', '2', '3', '4', '5', '6',
                    '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
            for (int idx = 0; idx < 6; ++idx) {
                byte b = result[idx];
                sb.append(digits[(b & 0xf0) >> 4]);
                sb.append(digits[b & 0x0f]);
            }
            return sb.toString();
        }

        private void paintCornersAsBitmap(Graphics2DImagePainter painter,
                Rectangle boundingBox, String name) throws IFException {
            //TODO parameters ok?
            ImageInfo info = new ImageInfo(name, null);

            ImageSize size = new ImageSize();
            size.setSizeInMillipoints(boundingBox.width, boundingBox.height);

            //Use the foreign attributes map to set image handling hints
            Map map = new java.util.HashMap(2);
            map.put(AFPForeignAttributeReader.RESOURCE_NAME, name);
            map.put(AFPForeignAttributeReader.RESOURCE_LEVEL, "print-file");

            AFPRenderingContext context = (AFPRenderingContext)
                    this.painter.createRenderingContext(/*map*/);

            size.setResolution(context.getPaintingState().getResolution());
            size.calcPixelsFromSize();
            info.setSize(size);
            ImageGraphics2D img = new ImageGraphics2D(info, painter);

            Map hints = new java.util.HashMap();

            hints.put(ImageHandlerUtil.CONVERSION_MODE, ImageHandlerUtil.CONVERSION_MODE_BITMAP);
            hints.put("TARGET_RESOLUTION",
                    context.getPaintingState().getResolution());


            try {
                this.painter.drawImage(img, boundingBox, context, true, hints);
            } catch (IOException ioe) {
                throw new IFException(
                        "I/O error while painting corner using a bitmap", ioe);
            } catch (ImageException ie) {
                throw new IFException(
                        "Image error while painting corner using a bitmap", ie);
            }
        }

        protected void arcTo(double startAngle, double endAngle, int cx, int cy, int width,
                int height) throws IOException {
            throw new UnsupportedOperationException("Can only deal with horizontal lines right now");

        }
    }

    /** {@inheritDoc} */
    @Override
    public void drawLine(Point start, Point end, int width, Color color, RuleStyle style)
            throws IFException {
        try {
            this.graphicsPainter.drawLine(start, end, width, color, style);
        } catch (IOException ioe) {
            throw new IFException("I/O error in drawLine()", ioe);
        }
    }

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

    private final class DefaultPtocaProducer implements PtocaProducer {
        final int[] coords;
        final int fontReference;
        final String text;
        final int[][] dp;
        final int letterSpacing;
        final int wordSpacing;
        final Font font;
        final AFPFont afpFont;
        final CharacterSet charSet;
        PresentationTextObject pto;

        private DefaultPtocaProducer(int x, int y,
                                      final int letterSpacing, final int wordSpacing, final int[][] dp,
                                      final String text) throws IFException {
            this.letterSpacing = letterSpacing;
            this.wordSpacing = wordSpacing;
            this.text = text;
            this.dp = dp;
            final int fontSize = state.getFontSize();
            getPaintingState().setFontSize(fontSize);

            FontTriplet triplet = new FontTriplet(
                    state.getFontFamily(), state.getFontStyle(), state.getFontWeight());
            //TODO Ignored: state.getFontVariant()
            String fontKey = getFontKey(triplet);

            // register font as necessary
            Map<String, Typeface> fontMetricMap = getFontInfo().getFonts();
            afpFont = (AFPFont) fontMetricMap.get(fontKey);
            font = getFontInfo().getFontInstance(triplet, fontSize);
            AFPPageFonts pageFonts = getPaintingState().getPageFonts();
            AFPFontAttributes fontAttributes = pageFonts.registerFont(fontKey, afpFont, fontSize);

            fontReference = fontAttributes.getFontReference();

            coords = unitConv.mpts2units(new float[] {x, y});

            charSet = afpFont.getCharacterSet(fontSize);

            if (afpFont.isEmbeddable()) {
                try {
                    getDocumentHandler().getResourceManager().embedFont(afpFont, charSet);
                } catch (IOException ioe) {
                    throw new IFException("Error while embedding font resources", ioe);
                }
            }

            AbstractPageObject page = getDataStream().getCurrentPage();

            try {
                if (bytesAvailable != null && bytesAvailable < getSize()) {
                    page.endPresentationObject();
                }
                pto = page.getPresentationTextObject();
                boolean success = pto.createControlSequences(this);
                if (!success) {
                    page.endPresentationObject();
                    pto = page.getPresentationTextObject();
                    pto.createControlSequences(this);
                }
            } catch (IOException ioe) {
                throw new IFException("I/O error in drawText()", ioe);
            }
        }

        private int getSize() throws IOException {
            final ByteArrayOutputStream bos = new ByteArrayOutputStream();
            PtocaBuilder pb = new PtocaBuilder() {
                protected OutputStream getOutputStreamForControlSequence(int length) {
                    return bos;
                }
            };
            produce(pb);
            return bos.size();
        }

        public void produce(PtocaBuilder builder) throws IOException {
            Point p = getPaintingState().getPoint(coords[X], coords[Y]);
            builder.setTextOrientation(getPaintingState().getRotation());
            builder.absoluteMoveBaseline(p.y);
            builder.absoluteMoveInline(p.x);

            builder.setExtendedTextColor(state.getTextColor());
            builder.setCodedFont((byte) fontReference);

            int l = text.length();
            int[] dx = IFUtil.convertDPToDX(dp);
            int dxl = (dx != null ? dx.length : 0);
            StringBuffer sb = new StringBuffer();

            if (dxl > 0 && dx[0] != 0) {
                int dxu = Math.round(unitConv.mpt2units(dx[0]));
                builder.relativeMoveInline(-dxu);
            }

            //Following are two variants for glyph placement.
            //SVI does not seem to be implemented in the same way everywhere, so
            //a fallback alternative is preserved here.
            final boolean usePTOCAWordSpacing = true;
            if (usePTOCAWordSpacing) {

                int interCharacterAdjustment = 0;
                if (letterSpacing != 0) {
                    interCharacterAdjustment = Math.round(unitConv.mpt2units(
                            letterSpacing));
                }
                builder.setInterCharacterAdjustment(interCharacterAdjustment);

                int spaceWidth = font.getCharWidth(CharUtilities.SPACE);
                int fixedSpaceCharacterIncrement = Math.round(unitConv.mpt2units(
                        spaceWidth + letterSpacing));
                int varSpaceCharacterIncrement = fixedSpaceCharacterIncrement;
                if (wordSpacing != 0) {
                    varSpaceCharacterIncrement = Math.round(unitConv.mpt2units(
                            spaceWidth + wordSpacing + letterSpacing));
                }
                builder.setVariableSpaceCharacterIncrement(varSpaceCharacterIncrement);

                boolean fixedSpaceMode = false;
                int ttPos = p.x;

                for (int i = 0; i < l; i++) {
                    char orgChar = text.charAt(i);
                    float glyphAdjust = 0;
                    if (afpFont.getFontType() == FontType.TRUETYPE) {
                        flushText(builder, sb, charSet);
                        fixedSpaceMode = true;
                        int charWidth = font.getCharWidth(orgChar);
                        sb.append(orgChar);
                        glyphAdjust += charWidth;
                    } else if (CharUtilities.isFixedWidthSpace(orgChar)) {
                        flushText(builder, sb, charSet);
                        builder.setVariableSpaceCharacterIncrement(
                                fixedSpaceCharacterIncrement);
                        fixedSpaceMode = true;
                        sb.append(CharUtilities.SPACE);
                        int charWidth = font.getCharWidth(orgChar);
                        glyphAdjust += (charWidth - spaceWidth);
                    } else {
                        if (fixedSpaceMode) {
                            flushText(builder, sb, charSet);
                            builder.setVariableSpaceCharacterIncrement(
                                    varSpaceCharacterIncrement);
                            fixedSpaceMode = false;
                        }
                        char ch;
                        if (orgChar == CharUtilities.NBSPACE) {
                            ch = ' '; //converted to normal space to allow word spacing
                        } else {
                            ch = orgChar;
                        }
                        sb.append(ch);
                    }

                    if (i < dxl - 1) {
                        glyphAdjust += dx[i + 1];
                    }

                    if (afpFont.getFontType() == FontType.TRUETYPE) {
                        flushText(builder, sb, charSet);
                        ttPos += Math.round(unitConv.mpt2units(glyphAdjust));
                        builder.absoluteMoveInline(ttPos);
                    } else if (glyphAdjust != 0) {
                        flushText(builder, sb, charSet);
                        int increment = Math.round(unitConv.mpt2units(glyphAdjust));
                        builder.relativeMoveInline(increment);
                    }
                }
            } else {
                for (int i = 0; i < l; i++) {
                    char orgChar = text.charAt(i);
                    float glyphAdjust = 0;
                    if (CharUtilities.isFixedWidthSpace(orgChar)) {
                        sb.append(CharUtilities.SPACE);
                        int spaceWidth = font.getCharWidth(CharUtilities.SPACE);
                        int charWidth = font.getCharWidth(orgChar);
                        glyphAdjust += (charWidth - spaceWidth);
                    } else {
                        sb.append(orgChar);
                    }

                    if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) {
                        glyphAdjust += wordSpacing;
                    }
                    glyphAdjust += letterSpacing;
                    if (i < dxl - 1) {
                        glyphAdjust += dx[i + 1];
                    }

                    if (glyphAdjust != 0) {
                        flushText(builder, sb, charSet);
                        int increment = Math.round(unitConv.mpt2units(glyphAdjust));
                        builder.relativeMoveInline(increment);
                    }
                }
            }
            flushText(builder, sb, charSet);
            if (pto != null) {
                bytesAvailable = pto.getBytesAvailable();
            }
        }

        private void flushText(PtocaBuilder builder, StringBuffer sb,
                               final CharacterSet charSet) throws IOException {
            if (sb.length() > 0) {
                builder.addTransparentData(charSet.encodeChars(sb));
                sb.setLength(0);
            }
        }

    }

    /**
     * Saves the graphics state of the rendering engine.
     * @throws IOException if an I/O error occurs
     */
    protected void saveGraphicsState() throws IOException {
        getPaintingState().save();
    }

    /**
     * Restores the last graphics state of the rendering engine.
     * @throws IOException if an I/O error occurs
     */
    protected void restoreGraphicsState() throws IOException {
        getPaintingState().restore();
    }


    /** {@inheritDoc} */
    public void clipBackground(Rectangle rect, BorderProps bpsBefore, BorderProps bpsAfter,
            BorderProps bpsStart, BorderProps bpsEnd) throws IFException {
    }

    /** {@inheritDoc} */
    public boolean isBackgroundRequired(BorderProps bpsBefore, BorderProps bpsAfter,
            BorderProps bpsStart, BorderProps bpsEnd) {
        return borderPainter.isBackgroundRequired(bpsBefore,  bpsAfter, bpsStart,  bpsEnd);
    }

    /** {@inheritDoc} */
    public void fillBackground(Rectangle rect, Paint fill, BorderProps bpsBefore,
            BorderProps bpsAfter, BorderProps bpsStart, BorderProps bpsEnd) throws IFException {
        // not supported in AFP
    }
}
