/*
 * Copyright (c) 2009, 2014, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.prism;

import com.sun.javafx.geom.Area;
import com.sun.javafx.geom.GeneralShapePair;
import com.sun.javafx.geom.Path2D;
import com.sun.javafx.geom.PathConsumer2D;
import com.sun.javafx.geom.PathIterator;
import com.sun.javafx.geom.RoundRectangle2D;
import com.sun.javafx.geom.Shape;
import com.sun.javafx.geom.ShapePair;
import com.sun.javafx.geom.transform.BaseTransform;
import com.sun.openpisces.Dasher;
import com.sun.openpisces.Stroker;
import com.sun.prism.impl.shape.OpenPiscesPrismUtils;

public final class BasicStroke {
    public static final int CAP_BUTT = Stroker.CAP_BUTT;
    public static final int CAP_ROUND = Stroker.CAP_ROUND;
    public static final int CAP_SQUARE = Stroker.CAP_SQUARE;

    public static final int JOIN_BEVEL = Stroker.JOIN_BEVEL;
    public static final int JOIN_MITER = Stroker.JOIN_MITER;
    public static final int JOIN_ROUND = Stroker.JOIN_ROUND;

    public static final int TYPE_CENTERED = 0;
    public static final int TYPE_INNER = 1;
    public static final int TYPE_OUTER = 2;

    float width;
    int type;
    int cap;
    int join;
    float miterLimit;
    float dash[];
    float dashPhase;

    public BasicStroke() {
        set(TYPE_CENTERED, 1.0f, CAP_SQUARE, JOIN_MITER, 10f);
    }

    public BasicStroke(float width, int cap, int join, float miterLimit) {
        set(TYPE_CENTERED, width, cap, join, miterLimit);
    }

    public BasicStroke(int type, float width,
                       int cap, int join, float miterLimit)
    {
        set(type, width, cap, join, miterLimit);
    }

    public BasicStroke(float width, int cap, int join, float miterLimit,
                       float[] dash, float dashPhase)
    {
        set(TYPE_CENTERED, width, cap, join, miterLimit);
        set(dash, dashPhase);
    }

    public BasicStroke(float width, int cap, int join, float miterLimit,
                       double[] dash, float dashPhase)
    {
        set(TYPE_CENTERED, width, cap, join, miterLimit);
        set(dash, dashPhase);
    }

    public BasicStroke(int type, float width, int cap, int join, float miterLimit,
                       float[] dash, float dashPhase)
    {
        set(type, width, cap, join, miterLimit);
        set(dash, dashPhase);
    }

    public BasicStroke(int type, float width, int cap, int join, float miterLimit,
                       double[] dash, float dashPhase)
    {
        set(type, width, cap, join, miterLimit);
        set(dash, dashPhase);
    }

    public void set(int type, float width,
                    int cap, int join, float miterLimit)
    {
        if (type != TYPE_CENTERED && type != TYPE_INNER && type != TYPE_OUTER) {
            throw new IllegalArgumentException("illegal type");
        }
        if (width < 0.0f) {
            throw new IllegalArgumentException("negative width");
        }
        if (cap != CAP_BUTT && cap != CAP_ROUND && cap != CAP_SQUARE) {
            throw new IllegalArgumentException("illegal end cap value");
        }
        if (join == JOIN_MITER) {
            if (miterLimit < 1.0f) {
                throw new IllegalArgumentException("miter limit < 1");
            }
        } else if (join != JOIN_ROUND && join != JOIN_BEVEL) {
            throw new IllegalArgumentException("illegal line join value");
        }
        this.type = type;
        this.width = width;
        this.cap = cap;
        this.join = join;
        this.miterLimit = miterLimit;
    }

    public void set(float dash[], float dashPhase) {
        if (dash != null) {
            boolean allzero = true;
            for (int i = 0; i < dash.length; i++) {
                float d = dash[i];
                if (d > 0.0) {
                    allzero = false;
                } else if (d < 0.0) {
                    throw new IllegalArgumentException("negative dash length");
                }
            }
            if (allzero) {
                throw new IllegalArgumentException("dash lengths all zero");
            }
        }
        this.dash = dash;
        this.dashPhase = dashPhase;
    }

    public void set(double dash[], float dashPhase) {
        if (dash != null) {
            float newdashes[] = new float[dash.length];
            boolean allzero = true;
            for (int i = 0; i < dash.length; i++) {
                float d = (float) dash[i];
                if (d > 0.0) {
                    allzero = false;
                } else if (d < 0.0) {
                    throw new IllegalArgumentException("negative dash length");
                }
                newdashes[i] = d;
            }
            if (allzero) {
                throw new IllegalArgumentException("dash lengths all zero");
            }
            this.dash = newdashes;
        } else {
            this.dash = null;
        }
        this.dashPhase = dashPhase;
    }

    /**
     * Returns the stroke type, one of {@code TYPE_CENTERED},
     * {@code TYPE_INNER}, or {@code TYPE_OUTER}.
     * @return the stroke type
     */
    public int getType() {
        return type;
    }

    /**
     * Returns the line width.  Line width is represented in user space,
     * which is the default-coordinate space used by Java 2D.  See the
     * <code>Graphics2D</code> class comments for more information on
     * the user space coordinate system.
     * @return the line width of this <code>BasicStroke</code>.
     */
    public float getLineWidth() {
        return width;
    }

    /**
     * Returns the end cap style.
     * @return the end cap style of this <code>BasicStroke</code> as one
     * of the static <code>int</code> values that define possible end cap
     * styles.
     */
    public int getEndCap() {
        return cap;
    }

    /**
     * Returns the line join style.
     * @return the line join style of the <code>BasicStroke</code> as one
     * of the static <code>int</code> values that define possible line
     * join styles.
     */
    public int getLineJoin() {
        return join;
    }

    /**
     * Returns the limit of miter joins.
     * @return the limit of miter joins of the <code>BasicStroke</code>.
     */
    public float getMiterLimit() {
        return miterLimit;
    }

    /**
     * Returns true if this stroke object will apply dashing attributes
     * to the path.
     * @return whether the stroke has dashes
     */
    public boolean isDashed() {
        return (dash != null);
    }
    /**
     * Returns the array representing the lengths of the dash segments.
     * Alternate entries in the array represent the user space lengths
     * of the opaque and transparent segments of the dashes.
     * As the pen moves along the outline of the <code>Shape</code>
     * to be stroked, the user space
     * distance that the pen travels is accumulated.  The distance
     * value is used to index into the dash array.
     * The pen is opaque when its current cumulative distance maps
     * to an even element of the dash array and transparent otherwise.
     * @return the dash array.
     */
    public float[] getDashArray() {
        return dash;
    }

    /**
     * Returns the current dash phase.
     * The dash phase is a distance specified in user coordinates that
     * represents an offset into the dashing pattern. In other words, the dash
     * phase defines the point in the dashing pattern that will correspond to
     * the beginning of the stroke.
     * @return the dash phase as a <code>float</code> value.
     */
    public float getDashPhase() {
        return dashPhase;
    }

    public Shape createStrokedShape(Shape s) {
        Shape ret;
        if (s instanceof RoundRectangle2D) {
            ret = strokeRoundRectangle((RoundRectangle2D) s);
        } else {
            ret = null;
        }
        if (ret != null) {
            return ret;
        }

        ret = createCenteredStrokedShape(s);

        if (type == TYPE_INNER) {
            ret = makeIntersectedShape(ret, s);
        } else if (type == TYPE_OUTER) {
            ret = makeSubtractedShape(ret, s);
        }
        return ret;
    }

    private boolean isCW(final float dx1, final float dy1,
                         final float dx2, final float dy2)
    {
        return dx1 * dy2 <= dy1 * dx2;
    }

    private void computeOffset(final float lx, final float ly,
                               final float w, final float[] m, int off) {
        final float len = (float) Math.sqrt(lx * lx + ly * ly);
        if (len == 0) {
            m[off + 0] = m[off + 1] = 0;
        } else {
            m[off + 0] = (ly * w) / len;
            m[off + 1] = -(lx * w) / len;
        }
    }

    private void computeMiter(final float x0, final float y0,
                              final float x1, final float y1,
                              final float x0p, final float y0p,
                              final float x1p, final float y1p,
                              final float[] m, int off)
    {
        float x10 = x1 - x0;
        float y10 = y1 - y0;
        float x10p = x1p - x0p;
        float y10p = y1p - y0p;

        // if this is 0, the lines are parallel. If they go in the
        // same direction, there is no intersection so m[off] and
        // m[off+1] will contain infinity, so no miter will be drawn.
        // If they go in the same direction that means that the start of the
        // current segment and the end of the previous segment have the same
        // tangent, in which case this method won't even be involved in
        // miter drawing because it won't be called by drawMiter (because
        // (mx == omx && my == omy) will be true, and drawMiter will return
        // immediately).
        float den = x10*y10p - x10p*y10;
        float t = x10p*(y0-y0p) - y10p*(x0-x0p);
        t /= den;
        m[off++] = x0 + t*x10;
        m[off] = y0 + t*y10;
    }


    // taken from com.sun.javafx.geom.Shape.accumulateQuad (added the width)
    private void accumulateQuad(float bbox[], int off,
                               float v0, float vc, float v1, float w)
    {
        // Breaking this quad down into a polynomial:
        // eqn[0] = v0;
        // eqn[1] = vc + vc - v0 - v0;
        // eqn[2] = v0 - vc - vc + v1;
        // Deriving the polynomial:
        // eqn'[0] = 1*eqn[1] = 2*(vc-v0)
        // eqn'[1] = 2*eqn[2] = 2*((v1-vc)-(vc-v0))
        // Solving for zeroes on the derivative:
        // e1*t + e0 = 0
        // t = -e0/e1;
        // t = -2(vc-v0) / 2((v1-vc)-(vc-v0))
        // t = (v0-vc) / (v1-vc+v0-vc)
        float num = v0 - vc;
        float den = v1 - vc + num;
        if (den != 0f) {
            float t = num / den;
            if (t > 0 && t < 1) {
                float u = 1f - t;
                float v = v0 * u * u + 2 * vc * t * u + v1 * t * t;
                if (bbox[off] > v - w) bbox[off] = v - w;
                if (bbox[off+2] < v + w) bbox[off+2] = v + w;
            }
        }
    }

    // taken from com.sun.javafx.geom.Shape.accumulateCubic (added the width)
    private void accumulateCubic(float bbox[], int off, float t,
                                float v0, float vc0, float vc1, float v1, float w)
    {
        if (t > 0 && t < 1) {
            float u = 1f - t;
            float v =        v0 * u * u * u
                      + 3 * vc0 * t * u * u
                      + 3 * vc1 * t * t * u
                      +      v1 * t * t * t;
            if (bbox[off] > v - w) bbox[off] = v - w;
            if (bbox[off+2] < v + w) bbox[off+2] = v + w;
        }
    }

    // taken from com.sun.javafx.geom.Shape.accumulateCubic (added the width)
    private void accumulateCubic(float bbox[], int off,
                                float v0, float vc0, float vc1, float v1, float w)
    {
        // Breaking this cubic down into a polynomial:
        // eqn[0] = v0;
        // eqn[1] = (vc0 - v0) * 3f;
        // eqn[2] = (vc1 - vc0 - vc0 + v0) * 3f;
        // eqn[3] = v1 + (vc0 - vc1) * 3f - v0;
        // Deriving the polynomial:
        // eqn'[0] = 1*eqn[1] = 3(vc0-v0)
        // eqn'[1] = 2*eqn[2] = 6((vc1-vc0)-(vc0-v0))
        // eqn'[2] = 3*eqn[3] = 3((v1-vc1)-2(vc1-vc0)+(vc0-v0))
        // Solving for zeroes on the derivative:
        // e2*t*t + e1*t + e0 = a*t*t + b*t + c = 0
        // Note that in solving for 0 we can divide all e0,e1,e2 by 3
        // t = (-b +/- sqrt(b*b-4ac))/2a
        float c = vc0 - v0;
        float b = 2f * ((vc1 - vc0) - c);
        float a = (v1 - vc1) - b - c;
        if (a == 0f) {
            // The quadratic parabola has degenerated to a line.
            if (b == 0f) {
                // The line has degenerated to a constant.
                return;
            }
            accumulateCubic(bbox, off, -c/b, v0, vc0, vc1, v1, w);
        } else {
            // From Numerical Recipes, 5.6, Quadratic and Cubic Equations
            float d = b * b - 4f * a * c;
            if (d < 0f) {
                // If d < 0.0, then there are no roots
                return;
            }
            d = (float) Math.sqrt(d);
            // For accuracy, calculate one root using:
            //     (-b +/- d) / 2a
            // and the other using:
            //     2c / (-b +/- d)
            // Choose the sign of the +/- so that b+d gets larger in magnitude
            if (b < 0f) {
                d = -d;
            }
            float q = (b + d) / -2f;
            // We already tested a for being 0 above
            accumulateCubic(bbox, off, q/a, v0, vc0, vc1, v1, w);
            if (q != 0f) {
                accumulateCubic(bbox, off, c/q, v0, vc0, vc1, v1, w);
            }
        }
    }

    // Basically any type of transform that does not violate a uniform
    // unsheared 2D scale.  We may have to scale the associated line width,
    // but we can accumulate everything in device space with no problems.
    private static final int SAFE_ACCUMULATE_MASK =
        (BaseTransform.TYPE_FLIP |
         BaseTransform.TYPE_GENERAL_ROTATION |
         BaseTransform.TYPE_QUADRANT_ROTATION |
         BaseTransform.TYPE_TRANSLATION |
         BaseTransform.TYPE_UNIFORM_SCALE);

    public void accumulateShapeBounds(float bbox[], Shape shape, BaseTransform tx) {
        if (type == TYPE_INNER) {
            Shape.accumulate(bbox, shape, tx);
            return;
        }
        if ((tx.getType() & ~SAFE_ACCUMULATE_MASK) != 0) {
            // This is a work-around for RT-15648.  That bug still applies here
            // since we should be optimizing that case, but at least with this
            // work-around, someone who calls this method, and is not aware of
            // that bug, will not be bitten by a bad answer.
            Shape.accumulate(bbox, createStrokedShape(shape), tx);
            return;
        }
        PathIterator pi = shape.getPathIterator(tx);
        boolean lastSegmentMove = true;
        float coords[] = new float[6];
        float w = type == TYPE_CENTERED ? getLineWidth() / 2 : getLineWidth();
        // Length(Transform(w, 0)) == w * Length(Transform(1, 0))
        w *= Math.hypot(tx.getMxx(), tx.getMyx());
        // starting x,y; previous x0, y0 and current x1,y1
        float sx = 0f, sy = 0f, x0 = 0f, y0 = 0f, x1, y1;
        // starting delta x,y; delta x,y; previous delta x,y
        float sdx = 0f, sdy = 0f, dx, dy, pdx = 0f, pdy = 0f;
        // current offset
        float o[] = new float[4];
        // previous offset; starting offset
        float pox = 0f, poy = 0f, sox = 0f, soy = 0f;

        while (!pi.isDone()) {
            int cur = pi.currentSegment(coords);
            switch (cur) {
                case PathIterator.SEG_MOVETO:
                    if (!lastSegmentMove) {
                        accumulateCap(pdx, pdy, x0, y0, pox, poy, bbox, w);
                        accumulateCap(-sdx, -sdy, sx, sy, -sox, -soy, bbox, w);
                    }

                    x0 = sx = coords[0];
                    y0 = sy = coords[1];
                    break;
                case PathIterator.SEG_LINETO:
                    x1 = coords[0];
                    y1 = coords[1];
                    dx = x1 - x0;
                    dy = y1 - y0;
                    if (dx == 0f && dy == 0f) {
                        // Ideally these segments should be ignored, but both
                        // Java 2D and OpenPisces treat this case as if we
                        // were joining to a segment that was horizontal.
                        dx = 1f;
                    }

                    computeOffset(dx, dy, w, o, 0);

                    if (!lastSegmentMove) {
                        accumulateJoin(pdx, pdy, dx, dy, x0, y0, pox, poy, o[0], o[1], bbox, w);
                    }

                    x0 = x1;
                    y0 = y1;
                    pdx = dx;
                    pdy = dy;
                    pox = o[0];
                    poy = o[1];
                    if (lastSegmentMove) {
                        sdx = pdx;
                        sdy = pdy;
                        sox = pox;
                        soy = poy;
                    }
                    break;
                case PathIterator.SEG_QUADTO:
                    x1 = coords[2];
                    y1 = coords[3];
                    dx = coords[0] - x0;
                    dy = coords[1] - y0;

                    computeOffset(dx, dy, w, o, 0);
                    if (!lastSegmentMove) {
                        accumulateJoin(pdx, pdy, dx, dy, x0, y0, pox, poy, o[0], o[1], bbox, w);
                    }

                    if (bbox[0] > coords[0] - w || bbox[2] < coords[0] + w) {
                        accumulateQuad(bbox, 0, x0, coords[0], x1, w);
                    }
                    if (bbox[1] > coords[1] - w || bbox[3] < coords[1] + w) {
                        accumulateQuad(bbox, 1, y0, coords[1], y1, w);
                    }
                    x0 = x1;
                    y0 = y1;
                    if (lastSegmentMove) {
                        sdx = dx;
                        sdy = dy;
                        sox = o[0];
                        soy = o[1];
                    }
                    pdx = x1 - coords[0];
                    pdy = y1 - coords[1];
                    computeOffset(pdx, pdy, w, o, 0);
                    pox = o[0];
                    poy = o[1];
                    break;
                case PathIterator.SEG_CUBICTO:
                    x1 = coords[4];
                    y1 = coords[5];
                    dx = coords[0] - x0;
                    dy = coords[1] - y0;

                    computeOffset(dx, dy, w, o, 0);
                    if (!lastSegmentMove) {
                        accumulateJoin(pdx, pdy, dx, dy, x0, y0, pox, poy, o[0], o[1], bbox, w);
                    }

                    if (bbox[0] > coords[0] - w || bbox[2] < coords[0] + w ||
                        bbox[0] > coords[2] - w || bbox[2] < coords[2] + w)
                    {
                        accumulateCubic(bbox, 0, x0, coords[0], coords[2], x1, w);
                    }
                    if (bbox[1] > coords[1] - w|| bbox[3] < coords[1] + w ||
                        bbox[1] > coords[3] - w|| bbox[3] < coords[3] + w)
                    {
                        accumulateCubic(bbox, 1, y0, coords[1], coords[3], y1, w);
                    }
                    x0 = x1;
                    y0 = y1;
                    if (lastSegmentMove) {
                        sdx = dx;
                        sdy = dy;
                        sox = o[0];
                        soy = o[1];
                    }
                    pdx = x1 - coords[2];
                    pdy = y1 - coords[3];
                    computeOffset(pdx, pdy, w, o, 0);
                    pox = o[0];
                    poy = o[1];
                    break;
                case PathIterator.SEG_CLOSE:
                    dx = sx - x0;
                    dy = sy - y0;
                    x1 = sx;
                    y1 = sy;

                    if (!lastSegmentMove) {
                        computeOffset(sdx, sdy, w, o, 2);
                        if (dx == 0 && dy == 0) {
                            accumulateJoin(pdx, pdy, sdx, sdy, sx, sy, pox, poy, o[2], o[3], bbox, w);
                        } else {
                            computeOffset(dx, dy, w, o, 0);
                            accumulateJoin(pdx, pdy, dx, dy, x0, y0, pox, poy, o[0], o[1], bbox, w);
                            accumulateJoin(dx, dy, sdx, sdy, x1, y1, o[0], o[1], o[2], o[3], bbox, w);
                        }
                    }
                    x0 = x1;
                    y0 = y1;
                    break;
            }
            // Close behaves like a move in certain ways - after close, line will draw a cap,
            // like if the close implicitely did move to the original coordinates
            lastSegmentMove = cur == PathIterator.SEG_MOVETO || cur == PathIterator.SEG_CLOSE;
            pi.next();
        }

        if (!lastSegmentMove) {
            accumulateCap(pdx, pdy, x0, y0, pox, poy, bbox, w);
            accumulateCap(-sdx, -sdy, sx, sy, -sox, -soy, bbox, w);
        }
    }

    private void accumulate(float o0, float o1, float o2, float o3, float[] bbox) {
        if (o0 <= o2) {
            if (o0 < bbox[0]) bbox[0] = o0;
            if (o2 > bbox[2]) bbox[2] = o2;
        } else {
            if (o2 < bbox[0]) bbox[0] = o2;
            if (o0 > bbox[2]) bbox[2] = o0;
        }
        if (o1 <= o3) {
            if (o1 < bbox[1]) bbox[1] = o1;
            if (o3 > bbox[3]) bbox[3] = o3;
        } else {
            if (o3 < bbox[1]) bbox[1] = o3;
            if (o1 > bbox[3]) bbox[3] = o1;
        }
    }

    private void accumulateOrdered(float o0, float o1, float o2, float o3, float[] bbox) {
        if (o0 < bbox[0]) bbox[0] = o0;
        if (o2 > bbox[2]) bbox[2] = o2;
        if (o1 < bbox[1]) bbox[1] = o1;
        if (o3 > bbox[3]) bbox[3] = o3;
    }


    private void accumulateJoin(float pdx, float pdy, float dx, float dy, float x0, float y0,
                                float pox, float poy, float ox, float oy, float[] bbox, float w) {

        if (join == JOIN_BEVEL) {
            accumulateBevel(x0, y0, pox, poy, ox, oy, bbox);
        } else if (join == JOIN_MITER) {
            accumulateMiter(pdx, pdy, dx, dy, pox, poy, ox, oy, x0, y0, bbox, w);
        } else { // JOIN_ROUND
            accumulateOrdered(x0 - w, y0 - w, x0 + w, y0 + w, bbox);
        }


    }

    private void accumulateCap(float dx, float dy, float x0, float y0,
                               float ox, float oy, float[] bbox, float w) {
        if (cap == CAP_SQUARE) {
            accumulate(x0 + ox - oy, y0 + oy + ox, x0 - ox - oy, y0 - oy + ox, bbox);
        } else if (cap == CAP_BUTT) {
            accumulate(x0 + ox, y0 + oy, x0 - ox, y0 - oy, bbox);
        } else { //cap == CAP_ROUND
            accumulateOrdered(x0 - w, y0 - w, x0 + w, y0 + w, bbox);
        }

    }

    private float[] tmpMiter = new float[2];

    private void accumulateMiter(float pdx, float pdy, float dx, float dy,
                                    float pox, float poy, float ox, float oy,
                                    float x0, float y0, float[] bbox, float w) {
        // Always accumulate bevel for cases of degenerate miters...
        accumulateBevel(x0, y0, pox, poy, ox, oy, bbox);

        boolean cw = isCW(pdx, pdy, dx, dy);

        if (cw) {
            pox = -pox;
            poy = -poy;
            ox = -ox;
            oy = -oy;
        }

        computeMiter((x0 - pdx) + pox, (y0 - pdy) + poy, x0 + pox, y0 + poy,
                     (x0 + dx) + ox, (y0 + dy) + oy, x0 + ox, y0 + oy,
                     tmpMiter, 0);
        float lenSq = (tmpMiter[0] - x0) * (tmpMiter[0] - x0) + (tmpMiter[1] - y0) * (tmpMiter[1] - y0);

        float miterLimitWidth = miterLimit * w;
        if (lenSq < miterLimitWidth * miterLimitWidth) {
            accumulateOrdered(tmpMiter[0], tmpMiter[1], tmpMiter[0], tmpMiter[1], bbox);
        }
    }


    private void accumulateBevel(float x0, float y0, float pox, float poy, float ox, float oy, float[] bbox) {
        accumulate(x0 + pox, y0 + poy, x0 - pox, y0 - poy, bbox);
        accumulate(x0 + ox, y0 + oy, x0 - ox, y0 - oy, bbox);
    }

    public Shape createCenteredStrokedShape(Shape s) {
        Path2D p2d = new Path2D(Path2D.WIND_NON_ZERO);
        float lw = (type == TYPE_CENTERED) ? width : width * 2.0f;
        PathConsumer2D pc2d =
            new Stroker(p2d, lw, cap, join, miterLimit);
        if (dash != null) {
            pc2d = new Dasher(pc2d, dash, dashPhase);
        }
        OpenPiscesPrismUtils.feedConsumer(s.getPathIterator(null), pc2d);
        return p2d;
    }

    static final float SQRT_2 = (float) Math.sqrt(2);

    Shape strokeRoundRectangle(RoundRectangle2D rr) {
        if (rr.width < 0 || rr.height < 0) {
            return new Path2D();
        }
        if (isDashed()) {
            return null;
        }
        int j;
        float aw = rr.arcWidth;
        float ah = rr.arcHeight;
        if (aw <= 0f || ah <= 0f) {
            aw = ah = 0f;
            if (type == TYPE_INNER) {
                j = JOIN_MITER;
            } else {
                j = this.join;
                if (j == JOIN_MITER && miterLimit < SQRT_2) {
                    j = JOIN_BEVEL;
                }
            }
        } else {
            if (aw < ah * 0.9f || ah < aw * 0.9f) {
                // RT-27416
                // TODO: Need to check these multipliers and
                // optimize this case...
                return null;
            }
            j = JOIN_ROUND;
        }
        float id, od;
        if (type == TYPE_INNER) {
            od = 0f;
            id = this.width;
        } else if (type == TYPE_OUTER) {
            od = this.width;
            id = 0f;
        } else {
            od = id = this.width/2f;
        }
        Shape outer;
        switch (j) {
            case JOIN_MITER:
                outer = new RoundRectangle2D(rr.x - od, rr.y - od,
                                             rr.width+od*2f, rr.height+od*2f,
                                             0f, 0f);
                break;
            case JOIN_BEVEL:
                outer = makeBeveledRect(rr.x, rr.y, rr.width, rr.height, od);
                break;
            case JOIN_ROUND:
                outer = new RoundRectangle2D(rr.x - od, rr.y - od,
                                             rr.width+od*2f, rr.height+od*2f,
                                             aw+od*2f, ah+od*2f);
                break;
            default:
                throw new InternalError("Unrecognized line join style");
        }
        if (rr.width <= id*2f || rr.height <= id*2f) {
            return outer;
        }
        aw -= id*2f;
        ah -= id*2f;
        if (aw <= 0f || ah <= 0f) {
            aw = ah = 0f;
        }
        Shape inner = new RoundRectangle2D(rr.x + id, rr.y + id,
                                           rr.width-id*2f, rr.height-id*2f,
                                           aw, ah);
        Path2D p2d = (outer instanceof Path2D)
            ? ((Path2D) outer) : new Path2D(outer);
        p2d.setWindingRule(Path2D.WIND_EVEN_ODD);
        p2d.append(inner, false);
        return p2d;
    }

    static Shape makeBeveledRect(float rx, float ry,
                                 float rw, float rh,
                                 float d)
    {
        float rx0 = rx;
        float ry0 = ry;
        float rx1 = rx + rw;
        float ry1 = ry + rh;
        Path2D p = new Path2D();
        p.moveTo(rx0, ry0 - d);
        p.lineTo(rx1, ry0 - d);
        p.lineTo(rx1 + d, ry0);
        p.lineTo(rx1 + d, ry1);
        p.lineTo(rx1, ry1 + d);
        p.lineTo(rx0, ry1 + d);
        p.lineTo(rx0 - d, ry1);
        p.lineTo(rx0 - d, ry0);
        p.closePath();
        return p;
    }

    protected Shape makeIntersectedShape(Shape outer, Shape inner) {
        return new CAGShapePair(outer, inner, ShapePair.TYPE_INTERSECT);
    }

    protected Shape makeSubtractedShape(Shape outer, Shape inner) {
        return new CAGShapePair(outer, inner, ShapePair.TYPE_SUBTRACT);
    }

    static class CAGShapePair extends GeneralShapePair {
        private Shape cagshape;

        public CAGShapePair(Shape outer, Shape inner, int type) {
            super(outer, inner, type);
        }

        @Override
        public PathIterator getPathIterator(BaseTransform tx) {
            if (cagshape == null) {
                Area o = new Area(getOuterShape());
                Area i = new Area(getInnerShape());
                if (getCombinationType() == ShapePair.TYPE_INTERSECT) {
                    o.intersect(i);
                } else {
                    o.subtract(i);
                }
                cagshape = o;
            }
            return cagshape.getPathIterator(tx);
        }
    }

    /**
     * Returns the hashcode for this stroke.
     * @return      a hash code for this stroke.
     */
    @Override
    public int hashCode() {
        int hash = Float.floatToIntBits(width);
        hash = hash * 31 + join;
        hash = hash * 31 + cap;
        hash = hash * 31 + Float.floatToIntBits(miterLimit);
        if (dash != null) {
            hash = hash * 31 + Float.floatToIntBits(dashPhase);
            for (int i = 0; i < dash.length; i++) {
                hash = hash * 31 + Float.floatToIntBits(dash[i]);
            }
        }
        return hash;
    }

    /**
     * Tests if a specified object is equal to this <code>BasicStroke</code>
     * by first testing if it is a <code>BasicStroke</code> and then comparing
     * its width, join, cap, miter limit, dash, and dash phase attributes with
     * those of this <code>BasicStroke</code>.
     * @param  obj the specified object to compare to this
     *              <code>BasicStroke</code>
     * @return <code>true</code> if the width, join, cap, miter limit, dash, and
     *            dash phase are the same for both objects;
     *            <code>false</code> otherwise.
     */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof BasicStroke)) {
            return false;
        }
        BasicStroke bs = (BasicStroke) obj;
        if (width != bs.width) {
            return false;
        }
        if (join != bs.join) {
            return false;
        }
        if (cap != bs.cap) {
            return false;
        }
        if (miterLimit != bs.miterLimit) {
            return false;
        }
        if (dash != null) {
            if (dashPhase != bs.dashPhase) {
                return false;
            }
            if (!java.util.Arrays.equals(dash, bs.dash)) {
                return false;
            }
        }
        else if (bs.dash != null) {
            return false;
        }

        return true;
    }

    public BasicStroke copy() {
        return new BasicStroke(type, width, cap, join, miterLimit, dash, dashPhase);
    }
}
