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

package org.apache.fop.render.intermediate;

import java.awt.Color;
import java.awt.Rectangle;
import java.io.IOException;

import org.apache.fop.traits.BorderProps;

/**
 * This is an abstract base class for handling border painting.
 */
public class BorderPainter {

    // TODO Use a class to model border instead of an array
    /** Convention index of before top */
    protected static final int TOP = 0;

    /** Convention index of right border */
    protected static final int RIGHT = 1;

    /** Convention index of bottom border */
    protected static final int BOTTOM = 2;

    /** Convention index of left border */
    protected static final int LEFT = 3;

    // TODO Use a class to model border corners instead of an array
    /** Convention index of top-left border corners */
    protected static final int TOP_LEFT = 0;

    /** Convention index of top-right-end border corners */
    protected static final int TOP_RIGHT = 1;

    /** Convention index of bottom-right border corners */
    protected static final int BOTTOM_RIGHT = 2;

    /** Convention index of bottom-left border corners */
    protected static final int BOTTOM_LEFT = 3;

    /** The ratio between a solid dash and the white-space in a dashed-border */
    public static final float DASHED_BORDER_SPACE_RATIO = 0.5f;
    /** The length of the dash as a factor of the border width i.e. 2 -&gt; dashWidth = 2*borderWidth */
    protected static final float DASHED_BORDER_LENGTH_FACTOR = 2.0f;

    private final GraphicsPainter graphicsPainter;

    public BorderPainter(GraphicsPainter graphicsPainter) {
        this.graphicsPainter = graphicsPainter;
    }

    /**
     * Draws borders.
     * @param borderRect the border rectangle
     * @param bpsTop the border specification on the top side
     * @param bpsBottom the border specification on the bottom side
     * @param bpsLeft the border specification on the left side
     * @param bpsRight the border specification on the end side
     * @param innerBackgroundColor the inner background color
     * @throws IFException if an error occurs while drawing the borders
     */
    public void drawBorders(Rectangle borderRect,
            BorderProps bpsTop, BorderProps bpsBottom,
            BorderProps bpsLeft, BorderProps bpsRight, Color innerBackgroundColor)
                throws IFException {
        try {
            drawRoundedBorders(borderRect, bpsTop, bpsBottom, bpsLeft, bpsRight);
        } catch (IOException ioe) {
            throw new IFException("IO error drawing borders", ioe);
        }
    }

    private BorderProps sanitizeBorderProps(BorderProps bps) {
        return bps == null ? bps : bps.width == 0 ? (BorderProps) null : bps;
    }

    /**
     * TODO merge with drawRoundedBorders()?
     * @param borderRect the border rectangle
     * @param bpsTop the border specification on the top side
     * @param bpsBottom the border specification on the bottom side
     * @param bpsLeft the border specification on the left side
     * @param bpsRight the border specification on the end side
     * @throws IOException
     */
    protected void drawRectangularBorders(Rectangle borderRect,
            BorderProps bpsTop, BorderProps bpsBottom,
            BorderProps bpsLeft, BorderProps bpsRight) throws IOException {

        bpsTop = sanitizeBorderProps(bpsTop);
        bpsBottom = sanitizeBorderProps(bpsBottom);
        bpsLeft = sanitizeBorderProps(bpsLeft);
        bpsRight = sanitizeBorderProps(bpsRight);


        int startx = borderRect.x;
        int starty = borderRect.y;
        int width = borderRect.width;
        int height = borderRect.height;
        boolean[] b = new boolean[] {
                (bpsTop != null), (bpsRight != null),
                (bpsBottom != null), (bpsLeft != null)};
        if (!b[TOP] && !b[RIGHT] && !b[BOTTOM] && !b[LEFT]) {
            return;
        }
        int[] bw = new int[] {
                (b[TOP] ? bpsTop.width : 0),
                (b[RIGHT] ? bpsRight.width : 0),
                (b[BOTTOM] ? bpsBottom.width : 0),
                (b[LEFT] ? bpsLeft.width : 0)};
        int[] clipw = new int[] {
                BorderProps.getClippedWidth(bpsTop),
                BorderProps.getClippedWidth(bpsRight),
                BorderProps.getClippedWidth(bpsBottom),
                BorderProps.getClippedWidth(bpsLeft)};
        starty += clipw[TOP];
        height -= clipw[TOP];
        height -= clipw[BOTTOM];
        startx += clipw[LEFT];
        width -= clipw[LEFT];
        width -= clipw[RIGHT];

        boolean[] slant = new boolean[] {
                (b[LEFT] && b[TOP]),
                (b[TOP] && b[RIGHT]),
                (b[RIGHT] && b[BOTTOM]),
                (b[BOTTOM] && b[LEFT])};
        if (bpsTop != null) {
            int sx1 = startx;
            int sx2 = (slant[TOP_LEFT] ? sx1 + bw[LEFT] - clipw[LEFT] : sx1);
            int ex1 = startx + width;
            int ex2 = (slant[TOP_RIGHT] ? ex1 - bw[RIGHT] + clipw[RIGHT] : ex1);
            int outery = starty - clipw[TOP];
            int clipy = outery + clipw[TOP];
            int innery = outery + bw[TOP];

            saveGraphicsState();
            moveTo(sx1, clipy);


            int sx1a = sx1;
            int ex1a = ex1;
            if (isCollapseOuter(bpsTop)) {
                if (isCollapseOuter(bpsLeft)) {
                    sx1a -= clipw[LEFT];
                }
                if (isCollapseOuter(bpsRight)) {
                    ex1a += clipw[RIGHT];
                }
                lineTo(sx1a, outery);
                lineTo(ex1a, outery);
            }
            lineTo(ex1, clipy);
            lineTo(ex2, innery);
            lineTo(sx2, innery);
            closePath();
            clip();
            drawBorderLine(sx1a, outery, ex1a, innery, true, true,
                    bpsTop.style, bpsTop.color);
            restoreGraphicsState();
        }
        if (bpsRight != null) {
            int sy1 = starty;
            int sy2 = (slant[TOP_RIGHT] ? sy1 + bw[TOP] - clipw[TOP] : sy1);
            int ey1 = starty + height;
            int ey2 = (slant[BOTTOM_RIGHT] ? ey1 - bw[BOTTOM] + clipw[BOTTOM] : ey1);
            int outerx = startx + width + clipw[RIGHT];
            int clipx = outerx - clipw[RIGHT];
            int innerx = outerx - bw[RIGHT];
            saveGraphicsState();
            moveTo(clipx, sy1);
            int sy1a = sy1;
            int ey1a = ey1;

            if (isCollapseOuter(bpsRight)) {
                if (isCollapseOuter(bpsTop)) {
                    sy1a -= clipw[TOP];
                }
                if (isCollapseOuter(bpsBottom)) {
                    ey1a += clipw[BOTTOM];
                }
                lineTo(outerx, sy1a);
                lineTo(outerx, ey1a);
            }
            lineTo(clipx, ey1);
            lineTo(innerx, ey2);
            lineTo(innerx, sy2);
            closePath();
            clip();
            drawBorderLine(innerx, sy1a, outerx, ey1a, false, false,
                           bpsRight.style, bpsRight.color);
            restoreGraphicsState();
        }
        if (bpsBottom != null) {
            int sx1 = startx;
            int sx2 = (slant[BOTTOM_LEFT] ? sx1 + bw[LEFT] - clipw[LEFT] : sx1);
            int ex1 = startx + width;
            int ex2 = (slant[BOTTOM_RIGHT] ? ex1 - bw[RIGHT] + clipw[RIGHT] : ex1);
            int outery = starty + height + clipw[BOTTOM];
            int clipy = outery - clipw[BOTTOM];
            int innery = outery - bw[BOTTOM];
            saveGraphicsState();
            moveTo(ex1, clipy);
            int sx1a = sx1;
            int ex1a = ex1;
            if (isCollapseOuter(bpsBottom)) {
                if (isCollapseOuter(bpsLeft)) {
                    sx1a -= clipw[LEFT];
                }
                if (isCollapseOuter(bpsRight)) {
                    ex1a += clipw[RIGHT];
                }
                lineTo(ex1a, outery);
                lineTo(sx1a, outery);
            }
            lineTo(sx1, clipy);
            lineTo(sx2, innery);
            lineTo(ex2, innery);
            closePath();
            clip();
            drawBorderLine(sx1a, innery, ex1a, outery, true, false,
                           bpsBottom.style, bpsBottom.color);
            restoreGraphicsState();
        }
        if (bpsLeft != null) {
            int sy1 = starty;
            int sy2 = (slant[TOP_LEFT] ? sy1 + bw[TOP] - clipw[TOP] : sy1);
            int ey1 = sy1 + height;
            int ey2 = (slant[BOTTOM_LEFT] ? ey1 - bw[BOTTOM] + clipw[BOTTOM] : ey1);
            int outerx = startx - clipw[LEFT];
            int clipx = outerx + clipw[LEFT];
            int innerx = outerx + bw[LEFT];

            saveGraphicsState();

            moveTo(clipx, ey1);

            int sy1a = sy1;
            int ey1a = ey1;
            if (isCollapseOuter(bpsLeft)) {
                if (isCollapseOuter(bpsTop)) {
                    sy1a -= clipw[TOP];
                }
                if (isCollapseOuter(bpsBottom)) {
                    ey1a += clipw[BOTTOM];
                }
                lineTo(outerx, ey1a);
                lineTo(outerx, sy1a);
            }
            lineTo(clipx, sy1);
            lineTo(innerx, sy2);
            lineTo(innerx, ey2);
            closePath();
            clip();
            drawBorderLine(outerx, sy1a, innerx, ey1a, false, true, bpsLeft.style, bpsLeft.color);
            restoreGraphicsState();
        }
    }

    private boolean isCollapseOuter(BorderProps bp) {
        return bp != null && bp.isCollapseOuter();
    }

    /**
     * This method calculates the length of the "dash" in a dashed border. The dash satisfies the
     * condition that corners start on a dash and end with a dash (rather than ending with a white space).
     * @param borderLength The length of the border.
     * @param borderWidth The width/thickness of the border.
     * @return returns the length of the dash such that it fits the criteria above.
     */
    public static float dashWidthCalculator(float borderLength, float borderWidth) {
        float dashWidth = DASHED_BORDER_LENGTH_FACTOR * borderWidth;
        if (borderWidth < 3) {
            dashWidth = (DASHED_BORDER_LENGTH_FACTOR * 3) * borderWidth;
        }
        int period = (int) ((borderLength - dashWidth) / dashWidth / (1.0f + DASHED_BORDER_SPACE_RATIO));
        period = period < 0 ? 0 : period;
        return borderLength / (period * (1.0f + DASHED_BORDER_SPACE_RATIO) + 1.0f);
    }

    /** TODO merge with drawRectangularBorders?
     * @param borderRect the border rectangle
     * @throws IOException on io exception
     * */
    protected void drawRoundedBorders(Rectangle borderRect,
            BorderProps beforeBorderProps, BorderProps afterBorderProps,
            BorderProps startBorderProps, BorderProps endBorderProps) throws IOException {
        BorderSegment before = borderSegmentForBefore(beforeBorderProps);
        BorderSegment after = borderSegmentForAfter(afterBorderProps);
        BorderSegment start = borderSegmentForStart(startBorderProps);
        BorderSegment end = borderSegmentForEnd(endBorderProps);
        if (before.getWidth() == 0 && after.getWidth() == 0 && start.getWidth() == 0 && end.getWidth() == 0) {
            return;
        }
        final int startx = borderRect.x + start.getClippedWidth();
        final int starty = borderRect.y + before.getClippedWidth();
        final int width = borderRect.width - start.getClippedWidth() - end.getClippedWidth();
        final int height = borderRect.height - before.getClippedWidth() - after.getClippedWidth();
        //Determine scale factor if any adjacent elliptic corners overlap
        double cornerCorrectionFactor = calculateCornerScaleCorrection(width, height, before, after, start,
                end);
        drawBorderSegment(start, before, end, 0, width, startx, starty, cornerCorrectionFactor);
        drawBorderSegment(before, end, after, 1, height, startx + width, starty, cornerCorrectionFactor);
        drawBorderSegment(end, after, start, 2, width, startx + width, starty + height,
                cornerCorrectionFactor);
        drawBorderSegment(after, start, before, 3, height, startx, starty + height, cornerCorrectionFactor);
    }

    private void drawBorderSegment(BorderSegment start, BorderSegment before, BorderSegment end,
            int orientation, int width, int x, int y, double cornerCorrectionFactor) throws IOException {
        if (before.getWidth() != 0) {
            //Let x increase in the START->END direction
            final int sx2 = start.getWidth() - start.getClippedWidth();
            final int ex1 =  width;
            final int ex2 = ex1 - end.getWidth() + end.getClippedWidth();
            final int outery = -before.getClippedWidth();
            final int innery = outery + before.getWidth();
            final int ellipseSBRadiusX = correctRadius(cornerCorrectionFactor, start.getRadiusEnd());
            final int ellipseSBRadiusY = correctRadius(cornerCorrectionFactor, before.getRadiusStart());
            final int ellipseBERadiusX = correctRadius(cornerCorrectionFactor, end.getRadiusStart());
            final int ellipseBERadiusY = correctRadius(cornerCorrectionFactor, before.getRadiusEnd());
            saveGraphicsState();
            translateCoordinates(x, y);
            if (orientation != 0) {
                rotateCoordinates(Math.PI * orientation / 2d);
            }
            final int ellipseSBX = ellipseSBRadiusX;
            final int ellipseSBY = ellipseSBRadiusY;
            final int ellipseBEX = ex1 - ellipseBERadiusX;
            final int ellipseBEY = ellipseBERadiusY;
            int sx1a = 0;
            int ex1a = ex1;
            if (ellipseSBRadiusX != 0 && ellipseSBRadiusY != 0) {
                final double[] joinMetrics = getCornerBorderJoinMetrics(ellipseSBRadiusX,
                        ellipseSBRadiusY, sx2, innery);
                final double outerJoinPointX = joinMetrics[0];
                final double outerJoinPointY = joinMetrics[1];
                final double sbJoinAngle = joinMetrics[2];
                moveTo((int) outerJoinPointX, (int) outerJoinPointY);
                arcTo(Math.PI + sbJoinAngle, Math.PI * 3 / 2,
                        ellipseSBX, ellipseSBY, ellipseSBRadiusX, ellipseSBRadiusY);
            }  else {
                moveTo(0, 0);
                if (before.isCollapseOuter()) {
                    if (start.isCollapseOuter()) {
                        sx1a -= start.getClippedWidth();
                    }
                    if (end.isCollapseOuter()) {
                        ex1a += end.getClippedWidth();
                    }
                    lineTo(sx1a, outery);
                    lineTo(ex1a, outery);
                }
            }
            if (ellipseBERadiusX != 0 && ellipseBERadiusY != 0) {
                final double[] outerJoinMetrics = getCornerBorderJoinMetrics(
                        ellipseBERadiusX, ellipseBERadiusY,  ex1 - ex2, innery);
                final double beJoinAngle = ex1 == ex2 ? Math.PI / 2 : Math.PI / 2 - outerJoinMetrics[2];
                lineTo(ellipseBEX, 0);
                arcTo(Math.PI * 3 / 2 , Math.PI * 3 / 2 + beJoinAngle,
                        ellipseBEX, ellipseBEY, ellipseBERadiusX, ellipseBERadiusY);
                if (ellipseBEX < ex2 && ellipseBEY > innery) {
                    final double[] innerJoinMetrics = getCornerBorderJoinMetrics(
                            (double) ex2 - ellipseBEX, (double) ellipseBEY - innery, ex1 - ex2, innery);
                    final double innerJoinPointX = innerJoinMetrics[0];
                    final double innerJoinPointY = innerJoinMetrics[1];
                    final double beInnerJoinAngle = Math.PI / 2 - innerJoinMetrics[2];
                    lineTo((int) (ex2 - innerJoinPointX), (int) (innerJoinPointY + innery));
                    arcTo(beInnerJoinAngle + Math.PI * 3 / 2, Math.PI * 3 / 2,
                            ellipseBEX, ellipseBEY, ex2 - ellipseBEX, ellipseBEY - innery);
                } else {
                    lineTo(ex2, innery);
                }
            } else {
                lineTo(ex1, 0);
                lineTo(ex2, innery);
            }
            if (ellipseSBRadiusX == 0) {
                lineTo(sx2, innery);
            } else {
                if (ellipseSBX > sx2 &&  ellipseSBY > innery) {
                    final double[] innerJoinMetrics = getCornerBorderJoinMetrics(ellipseSBRadiusX - sx2,
                            ellipseSBRadiusY - innery, sx2, innery);
                    final double sbInnerJoinAngle = innerJoinMetrics[2];
                    lineTo(ellipseSBX, innery);
                    arcTo(Math.PI * 3 / 2, sbInnerJoinAngle + Math.PI,
                            ellipseSBX, ellipseSBY, ellipseSBX - sx2, ellipseSBY - innery);
                } else {
                    lineTo(sx2, innery);
                }
            }
            closePath();
            clip();
            if (ellipseBERadiusY == 0 && ellipseSBRadiusY == 0) {
                drawBorderLine(sx1a, outery, ex1a, innery, true, true,
                        before.getStyle(), before.getColor());
            } else {
                int innerFillY = Math.max(Math.max(ellipseBEY, ellipseSBY), innery);
                drawBorderLine(sx1a, outery, ex1a, innerFillY, true, true,
                        before.getStyle(), before.getColor());
            }
            restoreGraphicsState();
        }
    }

    private static int correctRadius(double cornerCorrectionFactor, int radius) {
        return (int) (Math.round(cornerCorrectionFactor * radius));
    }

    private static BorderSegment borderSegmentForBefore(BorderProps before) {
        return AbstractBorderSegment.asBorderSegment(before);
    }

    private static BorderSegment borderSegmentForAfter(BorderProps after) {
        return AbstractBorderSegment.asFlippedBorderSegment(after);
    }

    private static BorderSegment borderSegmentForStart(BorderProps start) {
        return AbstractBorderSegment.asFlippedBorderSegment(start);
    }

    private static BorderSegment borderSegmentForEnd(BorderProps end) {
        return AbstractBorderSegment.asBorderSegment(end);
    }

    private interface BorderSegment {

        Color getColor();

        int getStyle();

        int getWidth();

        int getClippedWidth();

        int getRadiusStart();

        int getRadiusEnd();

        boolean isCollapseOuter();

        boolean isSpecified();
    }

    private abstract static class AbstractBorderSegment implements BorderSegment {

        private static BorderSegment asBorderSegment(BorderProps borderProps) {
            return borderProps == null ? NullBorderSegment.INSTANCE : new WrappingBorderSegment(borderProps);
        }

        private static BorderSegment asFlippedBorderSegment(BorderProps borderProps) {
            return borderProps == null ? NullBorderSegment.INSTANCE : new FlippedBorderSegment(borderProps);
        }

        public boolean isSpecified() {
            return !(this instanceof NullBorderSegment);
        }

        private static class WrappingBorderSegment extends AbstractBorderSegment {

            protected final BorderProps borderProps;

            private final int clippedWidth;

            WrappingBorderSegment(BorderProps borderProps) {
                this.borderProps = borderProps;
                clippedWidth = BorderProps.getClippedWidth(borderProps);
            }

            public int getStyle() {
                return borderProps.style;
            }

            public Color getColor() {
                return borderProps.color;
            }

            public int getWidth() {
                return borderProps.width;
            }

            public int getClippedWidth() {
                return clippedWidth;
            }
            public boolean isCollapseOuter() {
                return borderProps.isCollapseOuter();
            }

            public int getRadiusStart() {
                return borderProps.getRadiusStart();
            }

            public int getRadiusEnd() {
                return borderProps.getRadiusEnd();
            }
        }

        private static class FlippedBorderSegment extends WrappingBorderSegment {

            FlippedBorderSegment(BorderProps borderProps) {
                super(borderProps);
            }

            public int getRadiusStart() {
                return borderProps.getRadiusEnd();
            }

            public int getRadiusEnd() {
                return borderProps.getRadiusStart();
            }
        }

        private static final class NullBorderSegment extends AbstractBorderSegment {

            public static final NullBorderSegment INSTANCE = new NullBorderSegment();

            private NullBorderSegment() {
            }

            public int getWidth() {
                return 0;
            }

            public int getClippedWidth() {
                return 0;
            }

            public int getRadiusStart() {
                return 0;
            }

            public int getRadiusEnd() {
                return 0;
            }

            public boolean isCollapseOuter() {
                return false;
            }

            public Color getColor() {
                throw new UnsupportedOperationException();
            }

            public int getStyle() {
                throw new UnsupportedOperationException();
            }

            public boolean isSpecified() {
                return false;
            }
        }
    }

    private double[] getCornerBorderJoinMetrics(double ellipseCenterX, double ellipseCenterY, double xWidth,
            double yWidth) {
        if (xWidth > 0) {
            return getCornerBorderJoinMetrics(ellipseCenterX, ellipseCenterY, yWidth / xWidth);
        } else {
            return new double[]{0, ellipseCenterY, 0};
        }
    }

    private double[] getCornerBorderJoinMetrics(double ellipseCenterX, double ellipseCenterY,
            double borderWidthRatio) {
        double x = ellipseCenterY * ellipseCenterX * (
                ellipseCenterY + ellipseCenterX * borderWidthRatio
                - Math.sqrt(2d * ellipseCenterX * ellipseCenterY * borderWidthRatio)
                ) / (ellipseCenterY * ellipseCenterY
                        + ellipseCenterX * ellipseCenterX * borderWidthRatio * borderWidthRatio);
        double y = borderWidthRatio * x;
        return new double[]{x, y, Math.atan((ellipseCenterY - y) / (ellipseCenterX - x))};
    }

    /**
     * Clip the background to the inner border
     * @param rect clipping rectangle
     * @param bpsBefore before border
     * @param bpsAfter after border
     * @param bpsStart start border
     * @param bpsEnd end border
     * @throws IOException if an I/O error occurs
     */
    public void clipBackground(Rectangle rect,
            BorderProps bpsBefore, BorderProps bpsAfter,
            BorderProps bpsStart, BorderProps bpsEnd) throws IOException {
        BorderSegment before = borderSegmentForBefore(bpsBefore);
        BorderSegment after = borderSegmentForAfter(bpsAfter);
        BorderSegment start = borderSegmentForStart(bpsStart);
        BorderSegment end = borderSegmentForEnd(bpsEnd);
        int startx = rect.x;
        int starty = rect.y;
        int width = rect.width;
        int height = rect.height;
        double correctionFactor = calculateCornerCorrectionFactor(width + start.getWidth() + end.getWidth(),
                height + before.getWidth() + after.getWidth(), bpsBefore, bpsAfter, bpsStart, bpsEnd);
        Corner cornerBeforeEnd = Corner.createBeforeEndCorner(before, end, correctionFactor);
        Corner cornerEndAfter = Corner.createEndAfterCorner(end, after, correctionFactor);
        Corner cornerAfterStart = Corner.createAfterStartCorner(after, start, correctionFactor);
        Corner cornerStartBefore = Corner.createStartBeforeCorner(start, before, correctionFactor);
        new PathPainter(startx + cornerStartBefore.radiusX, starty)
                .lineHorizTo(width - cornerStartBefore.radiusX - cornerBeforeEnd.radiusX)
                .drawCorner(cornerBeforeEnd)
                .lineVertTo(height - cornerBeforeEnd.radiusY - cornerEndAfter.radiusY)
                .drawCorner(cornerEndAfter)
                .lineHorizTo(cornerEndAfter.radiusX + cornerAfterStart.radiusX - width)
                .drawCorner(cornerAfterStart)
                .lineVertTo(cornerAfterStart.radiusY + cornerStartBefore.radiusY - height)
                .drawCorner(cornerStartBefore);
        clip();
    }



    /**
     * The four corners
     *      SB - Start-Before
     *      BE - Before-End
     *      EA - End-After
     *      AS - After-Start
     *
     * 0 --> x
     * |
     * v
     * y
     *
     *  SB      BE
     *    *----*
     *    |    |
     *    |    |
     *    *----*
     *  AS      EA
     *
     */
    private enum CornerAngles {
        /** The before-end angles */
        BEFORE_END(Math.PI * 3 / 2, 0),
        /** The end-after angles */
        END_AFTER(0, Math.PI / 2),
        /** The after-start angles*/
        AFTER_START(Math.PI / 2, Math.PI),
        /** The start-before angles */
        START_BEFORE(Math.PI, Math.PI * 3 / 2);

        /** Angle of the start of the corner arch relative to the x-axis in the counter-clockwise direction */
        private final double start;

        /** Angle of the end of the corner arch relative to the x-axis in the counter-clockwise direction */
        private final double end;

        CornerAngles(double start, double end) {
            this.start = start;
            this.end = end;
        }

    }

    private static final class Corner {

        private static final Corner SQUARE = new Corner(0, 0, null, 0, 0, 0, 0);

        /** The radius of the elliptic corner in the x direction */
        private final int radiusX;

        /** The radius of the elliptic corner in the y direction */
        private final int radiusY;

        /** The start and end angles of the corner ellipse */
        private final CornerAngles angles;

        /** The offset in the x direction of the center of the ellipse relative to the starting point */
        private final int centerX;

        /** The offset in the y direction of the center of the ellipse relative to the starting point */
        private final int centerY;

        /** The value in the x direction that the corner extends relative to the starting point */
        private final int incrementX;

        /** The value in the y direction that the corner extends relative to the starting point */
        private final int incrementY;

        private Corner(int radiusX, int radiusY, CornerAngles angles, int ellipseOffsetX,
                int ellipseOffsetY, int incrementX, int incrementY) {
            this.radiusX = radiusX;
            this.radiusY = radiusY;
            this.angles = angles;
            this.centerX = ellipseOffsetX;
            this.centerY = ellipseOffsetY;
            this.incrementX = incrementX;
            this.incrementY = incrementY;
        }

        private static int extentFromRadiusStart(BorderSegment border, double correctionFactor) {
            return extentFromRadius(border.getRadiusStart(), border, correctionFactor);
        }

        private static int extentFromRadiusEnd(BorderSegment border, double correctionFactor) {
            return extentFromRadius(border.getRadiusEnd(), border, correctionFactor);
        }

        private static int extentFromRadius(int radius, BorderSegment border, double correctionFactor) {
            return Math.max((int) (radius * correctionFactor) - border.getWidth(), 0);
        }

        public static Corner createBeforeEndCorner(BorderSegment before, BorderSegment end,
                double correctionFactor) {
            int width = end.getRadiusStart();
            int height = before.getRadiusEnd();
            if (width == 0 || height == 0) {
                return SQUARE;
            }
            int x = extentFromRadiusStart(end, correctionFactor);
            int y = extentFromRadiusEnd(before, correctionFactor);
            return new Corner(x, y, CornerAngles.BEFORE_END, 0, y, x, y);
        }

        public static Corner createEndAfterCorner(BorderSegment end, BorderSegment after,
                double correctionFactor) {
            int width = end.getRadiusEnd();
            int height = after.getRadiusStart();
            if (width == 0 || height == 0) {
                return SQUARE;
            }
            int x = extentFromRadiusEnd(end, correctionFactor);
            int y = extentFromRadiusStart(after, correctionFactor);
            return new Corner(x, y, CornerAngles.END_AFTER, -x, 0, -x, y);
        }

        public static Corner createAfterStartCorner(BorderSegment after, BorderSegment start,
                double correctionFactor) {
            int width = start.getRadiusStart();
            int height = after.getRadiusEnd();
            if (width == 0 || height == 0) {
                return SQUARE;
            }
            int x = extentFromRadiusStart(start, correctionFactor);
            int y = extentFromRadiusEnd(after, correctionFactor);
            return new Corner(x, y, CornerAngles.AFTER_START, 0, -y, -x, -y);
        }

        public static Corner createStartBeforeCorner(BorderSegment start, BorderSegment before,
                double correctionFactor) {
            int width = start.getRadiusEnd();
            int height = before.getRadiusStart();
            if (width == 0 || height == 0) {
                return SQUARE;
            }
            int x = extentFromRadiusEnd(start, correctionFactor);
            int y = extentFromRadiusStart(before, correctionFactor);
            return new Corner(x, y, CornerAngles.START_BEFORE, x, 0, x, -y);
        }
    }

    /**
     * This is a helper class for constructing curves composed of move, line and arc operations.  Coordinates
     * are relative to the terminal point of the previous operation
     */
    private final class PathPainter {

        /** Current x position */
        private int x;

        /** Current y position */
        private int y;

        PathPainter(int x, int y) throws IOException {
            moveTo(x, y);
        }

        private void moveTo(int x, int y) throws IOException {
            this.x += x;
            this.y += y;
            BorderPainter.this.moveTo(this.x, this.y);
        }

        public PathPainter lineTo(int x, int y) throws IOException {
            this.x += x;
            this.y += y;
            BorderPainter.this.lineTo(this.x, this.y);
            return this;
        }

        public PathPainter lineHorizTo(int x) throws IOException {
            return lineTo(x, 0);
        }

        public PathPainter lineVertTo(int y) throws IOException {
            return lineTo(0, y);
        }

        PathPainter drawCorner(Corner corner) throws IOException {
            if (corner.radiusX == 0 && corner.radiusY == 0) {
                return this;
            }
            if (corner.radiusX == 0 || corner.radiusY == 0) {
                x += corner.incrementX;
                y += corner.incrementY;
                BorderPainter.this.lineTo(x, y);
                return this;
            }
            BorderPainter.this.arcTo(corner.angles.start, corner.angles.end, x + corner.centerX,
                    y + corner.centerY, corner.radiusX, corner.radiusY);
            x += corner.incrementX;
            y += corner.incrementY;
            return this;
        }
    }

    /**
     * Calculate the correction factor to handle over-sized elliptic corner radii.
     *
     * @param width the border width
     * @param height the border height
     * @param before the before border properties
     * @param after the after border properties
     * @param start the start border properties
     * @param end the end border properties
     *
     */
    protected static double calculateCornerCorrectionFactor(int width, int height, BorderProps before,
            BorderProps after, BorderProps start, BorderProps end) {
        return calculateCornerScaleCorrection(width, height, borderSegmentForBefore(before),
                borderSegmentForAfter(after), borderSegmentForStart(start), borderSegmentForEnd(end));
    }

    /**
     * Calculate the scaling factor to handle over-sized elliptic corner radii.
     *
     * @param width the border width
     * @param height the border height
     * @param before the before border segment
     * @param after the after border segment
     * @param start the start border segment
     * @param end the end border segment
     */
    protected static double calculateCornerScaleCorrection(int width, int height, BorderSegment before,
            BorderSegment after, BorderSegment start, BorderSegment end) {
        return CornerScaleCorrectionCalculator.calculate(width, height, before, after, start, end);
    }

    private static final class CornerScaleCorrectionCalculator {

        private double correctionFactor = 1;

        private CornerScaleCorrectionCalculator(int width, int height,
                BorderSegment before, BorderSegment after,
                BorderSegment start, BorderSegment end) {
            calculateForSegment(width, start, before, end);
            calculateForSegment(height, before, end, after);
            calculateForSegment(width, end, after, start);
            calculateForSegment(height, after, start, before);
        }

        public static double calculate(int width, int height,
                BorderSegment before, BorderSegment after,
                BorderSegment start, BorderSegment end) {
            return new CornerScaleCorrectionCalculator(width, height, before, after, start, end)
                    .correctionFactor;
        }

        private void calculateForSegment(int width, BorderSegment bpsStart, BorderSegment bpsBefore,
                BorderSegment bpsEnd) {
            if (bpsBefore.isSpecified()) {
                double ellipseExtent = bpsStart.getRadiusEnd() + bpsEnd.getRadiusStart();
                if (ellipseExtent > 0) {
                    double thisCorrectionFactor = width / ellipseExtent;
                    if (thisCorrectionFactor < correctionFactor) {
                        correctionFactor = thisCorrectionFactor;
                    }
                }
            }
        }
    }

    private void drawBorderLine(int x1, int y1, int x2, int y2, boolean horz, boolean startOrBefore,
            int style, Color color) throws IOException {
        graphicsPainter.drawBorderLine(x1, y1, x2, y2, horz, startOrBefore, style, color);
    }

    private void moveTo(int x, int y) throws IOException {
        graphicsPainter.moveTo(x, y);
    }

    private void lineTo(int x, int y) throws IOException {
        graphicsPainter.lineTo(x, y);
    }

    private void arcTo(final double startAngle, final double endAngle, final int cx, final int cy,
            final int width, final int height) throws IOException {
        graphicsPainter.arcTo(startAngle, endAngle, cx, cy, width, height);
    }

    private void rotateCoordinates(double angle) throws IOException {
        graphicsPainter.rotateCoordinates(angle);
    }

    private void translateCoordinates(int xTranslate, int yTranslate) throws IOException {
        graphicsPainter.translateCoordinates(xTranslate, yTranslate);
    }

    private void closePath() throws IOException {
        graphicsPainter.closePath();
    }

    private void clip() throws IOException {
        graphicsPainter.clip();
    }

    private void saveGraphicsState() throws IOException {
        graphicsPainter.saveGraphicsState();
    }

    private void restoreGraphicsState() throws IOException {
        graphicsPainter.restoreGraphicsState();
    }

}
