/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   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 .
 */

#include <basegfx/polygon/b2dpolygontools.hxx>
#include <basegfx/polygon/b2dpolypolygontools.hxx>
#include <basegfx/polygon/b2dpolypolygon.hxx>
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <basegfx/matrix/b2dhommatrixtools.hxx>

#include <rtl/ustring.hxx>
#include <sal/log.hxx>
#include <rtl/math.hxx>
#include <rtl/character.hxx>
#include <stringconversiontools.hxx>

namespace
{

void putCommandChar(OUStringBuffer& rBuffer,sal_Unicode& rLastSVGCommand, sal_Unicode aChar, bool bToLower,bool bVerbose)
{
    const sal_Unicode aCommand = bToLower ? rtl::toAsciiLowerCase(aChar) : aChar;

    if (bVerbose && rBuffer.getLength())
        rBuffer.append(' ');

    if (bVerbose || rLastSVGCommand != aCommand)
    {
        rBuffer.append(aCommand);
        rLastSVGCommand = aCommand;
    }
}

void putNumberChar(OUStringBuffer& rStr,double fValue, double fOldValue, bool bUseRelativeCoordinates,bool bVerbose)
{
    if (bUseRelativeCoordinates)
        fValue -= fOldValue;

    const sal_Int32 aLen(rStr.getLength());
    if (bVerbose || (aLen && basegfx::internal::isOnNumberChar(rStr[aLen - 1], false) && fValue >= 0.0))
        rStr.append(' ');

    rStr.append(fValue);
}

}

namespace basegfx::utils
{
        bool PointIndex::operator<(const PointIndex& rComp) const
        {
            if(rComp.getPolygonIndex() == getPolygonIndex())
            {
                return rComp.getPointIndex() < getPointIndex();
            }

            return rComp.getPolygonIndex() < getPolygonIndex();
        }

        bool importFromSvgD(
            B2DPolyPolygon& o_rPolyPolygon,
            std::u16string_view rSvgDStatement,
            bool bHandleRelativeNextPointCompatible,
            PointIndexSet* pHelpPointIndexSet)
        {
            o_rPolyPolygon.clear();
            const sal_Int32 nLen(rSvgDStatement.size());
            sal_Int32 nPos(0);
            double nLastX( 0.0 );
            double nLastY( 0.0 );
            B2DPolygon aCurrPoly;

            // skip initial whitespace
            basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

            while(nPos < nLen)
            {
                bool bRelative(false);
                const sal_Unicode aCurrChar(rSvgDStatement[nPos]);

                if(o_rPolyPolygon.count() && !aCurrPoly.count() && aCurrChar != 'm' && aCurrChar != 'M')
                {
                    // we have a new sub-polygon starting, but without a 'moveto' command.
                    // this requires to add the current point as start point to the polygon
                    // (see SVG1.1 8.3.3 The "closepath" command)
                    aCurrPoly.append(B2DPoint(nLastX, nLastY));
                }

                switch(aCurrChar)
                {
                    case 'z' :
                    case 'Z' :
                    {
                        // consume CurrChar and whitespace
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        // create closed polygon and reset import values
                        if(aCurrPoly.count())
                        {
                            if(!bHandleRelativeNextPointCompatible)
                            {
                                // SVG defines that "the next subpath starts at the
                                // same initial point as the current subpath", so set the
                                // current point if we do not need to be compatible
                                nLastX = aCurrPoly.getB2DPoint(0).getX();
                                nLastY = aCurrPoly.getB2DPoint(0).getY();
                            }

                            aCurrPoly.setClosed(true);
                            o_rPolyPolygon.append(aCurrPoly);
                            aCurrPoly.clear();
                        }

                        break;
                    }

                    case 'm' :
                    case 'M' :
                    {
                        // create non-closed polygon and reset import values
                        if(aCurrPoly.count())
                        {
                            o_rPolyPolygon.append(aCurrPoly);
                            aCurrPoly.clear();
                        }
                        [[fallthrough]]; // to add coordinate data as 1st point of new polygon
                    }
                    case 'l' :
                    case 'L' :
                    {
                        if(aCurrChar == 'm' || aCurrChar == 'l')
                        {
                            bRelative = true;
                        }

                        // consume CurrChar and whitespace
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX, nY;

                            if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nX += nLastX;
                                nY += nLastY;
                            }

                            // set last position
                            nLastX = nX;
                            nLastY = nY;

                            // add point
                            aCurrPoly.append(B2DPoint(nX, nY));
                        }
                        break;
                    }

                    case 'h' :
                    {
                        bRelative = true;
                        [[fallthrough]];
                    }
                    case 'H' :
                    {
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX, nY(nLastY);

                            if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nX += nLastX;
                            }

                            // set last position
                            nLastX = nX;

                            // add point
                            aCurrPoly.append(B2DPoint(nX, nY));
                        }
                        break;
                    }

                    case 'v' :
                    {
                        bRelative = true;
                        [[fallthrough]];
                    }
                    case 'V' :
                    {
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX(nLastX), nY;

                            if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nY += nLastY;
                            }

                            // set last position
                            nLastY = nY;

                            // add point
                            aCurrPoly.append(B2DPoint(nX, nY));
                        }
                        break;
                    }

                    case 's' :
                    {
                        bRelative = true;
                        [[fallthrough]];
                    }
                    case 'S' :
                    {
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX, nY;
                            double nX2, nY2;

                            if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nX2 += nLastX;
                                nY2 += nLastY;
                                nX += nLastX;
                                nY += nLastY;
                            }

                            // ensure existence of start point
                            sal_uInt32 nCurrPolyCount = aCurrPoly.count();
                            if (nCurrPolyCount == 0)
                            {
                                aCurrPoly.append(B2DPoint(nLastX, nLastY));
                                nCurrPolyCount = 1;
                            }
                            assert(nCurrPolyCount > 0 && "coverity 2023.12.2");

                            // get first control point. It's the reflection of the PrevControlPoint
                            // of the last point. If not existent, use current point (see SVG)
                            B2DPoint aPrevControl(nLastX, nLastY);
                            const sal_uInt32 nIndex(nCurrPolyCount - 1);

                            if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex))
                            {
                                const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex));
                                const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex));

                                // use mirrored previous control point
                                aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX());
                                aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY());
                            }

                            // append curved edge
                            aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2, nY2), B2DPoint(nX, nY));

                            // set last position
                            nLastX = nX;
                            nLastY = nY;
                        }
                        break;
                    }

                    case 'c' :
                    {
                        bRelative = true;
                        [[fallthrough]];
                    }
                    case 'C' :
                    {
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX, nY;
                            double nX1, nY1;
                            double nX2, nY2;

                            if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nX1 += nLastX;
                                nY1 += nLastY;
                                nX2 += nLastX;
                                nY2 += nLastY;
                                nX += nLastX;
                                nY += nLastY;
                            }

                            // ensure existence of start point
                            if(!aCurrPoly.count())
                            {
                                aCurrPoly.append(B2DPoint(nLastX, nLastY));
                            }

                            // append curved edge
                            aCurrPoly.appendBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX2, nY2), B2DPoint(nX, nY));

                            // set last position
                            nLastX = nX;
                            nLastY = nY;
                        }
                        break;
                    }

                    // #100617# quadratic beziers are imported as cubic ones
                    case 'q' :
                    {
                        bRelative = true;
                        [[fallthrough]];
                    }
                    case 'Q' :
                    {
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX, nY;
                            double nX1, nY1;

                            if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nX1 += nLastX;
                                nY1 += nLastY;
                                nX += nLastX;
                                nY += nLastY;
                            }

                            // ensure existence of start point
                            if(!aCurrPoly.count())
                            {
                                aCurrPoly.append(B2DPoint(nLastX, nLastY));
                            }

                            // append curved edge
                            aCurrPoly.appendQuadraticBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX, nY));

                            // set last position
                            nLastX = nX;
                            nLastY = nY;
                        }
                        break;
                    }

                    // #100617# relative quadratic beziers are imported as cubic
                    case 't' :
                    {
                        bRelative = true;
                        [[fallthrough]];
                    }
                    case 'T' :
                    {
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX, nY;

                            if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nX += nLastX;
                                nY += nLastY;
                            }

                            // ensure existence of start point
                            sal_uInt32 nCurrPolyCount = aCurrPoly.count();
                            if (nCurrPolyCount == 0)
                            {
                                aCurrPoly.append(B2DPoint(nLastX, nLastY));
                                nCurrPolyCount = 1;
                            }
                            assert(nCurrPolyCount > 0 && "coverity 2023.12.2");

                            // get first control point. It's the reflection of the PrevControlPoint
                            // of the last point. If not existent, use current point (see SVG)
                            B2DPoint aPrevControl(nLastX, nLastY);
                            const sal_uInt32 nIndex(nCurrPolyCount - 1);
                            const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex));

                            if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex))
                            {
                                const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex));

                                // use mirrored previous control point
                                aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX());
                                aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY());
                            }

                            if(!aPrevControl.equal(aPrevPoint))
                            {
                                // there is a prev control point, and we have the already mirrored one
                                // in aPrevControl. We also need the quadratic control point for this
                                // new quadratic segment to calculate the 2nd cubic control point
                                const B2DPoint aQuadControlPoint(
                                    ((3.0 * aPrevControl.getX()) - aPrevPoint.getX()) / 2.0,
                                    ((3.0 * aPrevControl.getY()) - aPrevPoint.getY()) / 2.0);

                                // calculate the cubic bezier coefficients from the quadratic ones.
                                const double nX2Prime((aQuadControlPoint.getX() * 2.0 + nX) / 3.0);
                                const double nY2Prime((aQuadControlPoint.getY() * 2.0 + nY) / 3.0);

                                // append curved edge, use mirrored cubic control point directly
                                aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2Prime, nY2Prime), B2DPoint(nX, nY));
                            }
                            else
                            {
                                // when no previous control, SVG says to use current point -> straight line.
                                // Just add end point
                                aCurrPoly.append(B2DPoint(nX, nY));
                            }

                            // set last position
                            nLastX = nX;
                            nLastY = nY;
                        }
                        break;
                    }

                    case 'a' :
                    {
                        bRelative = true;
                        [[fallthrough]];
                    }
                    case 'A' :
                    {
                        nPos++;
                        basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen);

                        while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos))
                        {
                            double nX, nY;
                            double fRX, fRY, fPhi;
                            sal_Int32 bLargeArcFlag, bSweepFlag;

                            if(!basegfx::internal::importDoubleAndSpaces(fRX, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(fRY, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(fPhi, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importFlagAndSpaces(bLargeArcFlag, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importFlagAndSpaces(bSweepFlag, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false;
                            if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false;

                            if(bRelative)
                            {
                                nX += nLastX;
                                nY += nLastY;
                            }

                            if( rtl::math::approxEqual(nX, nLastX) && rtl::math::approxEqual(nY, nLastY) )
                                continue; // start==end -> skip according to SVG spec

                            if( fRX == 0.0 || fRY == 0.0 )
                            {
                                // straight line segment according to SVG spec
                                aCurrPoly.append(B2DPoint(nX, nY));
                            }
                            else
                            {
                                // normalize according to SVG spec
                                fRX=fabs(fRX); fRY=fabs(fRY);

                                // from the SVG spec, appendix F.6.4

                                // |x1'|   |cos phi   sin phi|  |(x1 - x2)/2|
                                // |y1'| = |-sin phi  cos phi|  |(y1 - y2)/2|
                                const B2DPoint p1(nLastX, nLastY);
                                const B2DPoint p2(nX, nY);
                                B2DHomMatrix aTransform(basegfx::utils::createRotateB2DHomMatrix(
                                    -deg2rad(fPhi)));

                                const B2DPoint p1_prime( aTransform * B2DPoint(((p1-p2)/2.0)) );

                                //           ______________________________________       rx y1'
                                // |cx'|  + /  rx^2 ry^2 - rx^2 y1'^2 - ry^2 x1^2           ry
                                // |cy'| =-/       rx^2y1'^2 + ry^2 x1'^2               - ry x1'
                                //                                                          rx
                                // chose + if f_A != f_S
                                // chose - if f_A  = f_S
                                B2DPoint aCenter_prime;
                                const double fRadicant(
                                    (fRX*fRX*fRY*fRY - fRX*fRX*p1_prime.getY()*p1_prime.getY() - fRY*fRY*p1_prime.getX()*p1_prime.getX())/
                                    (fRX*fRX*p1_prime.getY()*p1_prime.getY() + fRY*fRY*p1_prime.getX()*p1_prime.getX()));
                                if( fRadicant < 0.0 )
                                {
                                    // no solution - according to SVG
                                    // spec, scale up ellipse
                                    // uniformly such that it passes
                                    // through end points (denominator
                                    // of radicant solved for fRY,
                                    // with s=fRX/fRY)
                                    const double fRatio(fRX/fRY);
                                    fRY=std::hypot(p1_prime.getY(), p1_prime.getX()/fRatio);
                                    fRX=fRatio*fRY;

                                    // keep center_prime forced to (0,0)
                                }
                                else
                                {
                                    const double fFactor(
                                        (bLargeArcFlag==bSweepFlag ? -1.0 : 1.0) *
                                        sqrt(fRadicant));

                                    // actually calculate center_prime
                                    aCenter_prime = B2DPoint(
                                        fFactor*fRX*p1_prime.getY()/fRY,
                                        -fFactor*fRY*p1_prime.getX()/fRX);
                                }

                                //              +           u - v
                                // angle(u,v) =  arccos( ------------ )     (take the sign of (ux vy - uy vx))
                                //              -        ||u|| ||v||

                                //                  1    | (x1' - cx')/rx |
                                // theta1 = angle((   ), |                | )
                                //                  0    | (y1' - cy')/ry |
                                const B2DPoint aRadii(fRX,fRY);
                                double fTheta1(
                                    B2DVector(1.0,0.0).angle(
                                        (p1_prime-aCenter_prime)/aRadii));

                                //                 |1|    |  (-x1' - cx')/rx |
                                // theta2 = angle( | | ,  |                  | )
                                //                 |0|    |  (-y1' - cy')/ry |
                                double fTheta2(
                                    B2DVector(1.0,0.0).angle(
                                        (-p1_prime-aCenter_prime)/aRadii));

                                // map both angles to [0,2pi)
                                fTheta1 = fmod(2*M_PI+fTheta1,2*M_PI);
                                fTheta2 = fmod(2*M_PI+fTheta2,2*M_PI);

                                // make sure the large arc is taken
                                // (since
                                // createPolygonFromEllipseSegment()
                                // normalizes to e.g. cw arc)
                                if( !bSweepFlag )
                                    std::swap(fTheta1,fTheta2);

                                // finally, create bezier polygon from this
                                B2DPolygon aSegment(
                                    utils::createPolygonFromUnitEllipseSegment(
                                        fTheta1, fTheta2 ));

                                // transform ellipse by rotation & move to final center
                                aTransform = basegfx::utils::createScaleB2DHomMatrix(fRX, fRY);
                                aTransform.translate(aCenter_prime.getX(),
                                                     aCenter_prime.getY());
                                aTransform.rotate(deg2rad(fPhi));
                                const B2DPoint aOffset((p1+p2)/2.0);
                                aTransform.translate(aOffset.getX(),
                                                     aOffset.getY());
                                aSegment.transform(aTransform);

                                // createPolygonFromEllipseSegment()
                                // always creates arcs that are
                                // positively oriented - flip polygon
                                // if we swapped angles above
                                if( !bSweepFlag )
                                    aSegment.flip();

                                // remember PointIndex of evtl. added pure helper points
                                sal_uInt32 nPointIndex(aCurrPoly.count() + 1);
                                aCurrPoly.append(aSegment);

                                // if asked for, mark pure helper points by adding them to the index list of
                                // helper points
                                if(pHelpPointIndexSet && aCurrPoly.count() > 1)
                                {
                                    const sal_uInt32 nPolyIndex(o_rPolyPolygon.count());

                                    for(;nPointIndex + 1 < aCurrPoly.count(); nPointIndex++)
                                    {
                                        pHelpPointIndexSet->insert(PointIndex(nPolyIndex, nPointIndex));
                                    }
                                }
                            }

                            // set last position
                            nLastX = nX;
                            nLastY = nY;
                        }
                        break;
                    }

                    default:
                    {
                        SAL_WARN("basegfx", "importFromSvgD(): skipping tags in svg:d element (unknown: \""
                                << OUString(aCurrChar)
                                << "\")!");
                        ++nPos;
                        break;
                    }
                }
            }

            // if there is polygon data, create non-closed polygon
            if(aCurrPoly.count())
            {
                o_rPolyPolygon.append(aCurrPoly);
            }

            return true;
        }

        bool importFromSvgPoints( B2DPolygon&            o_rPoly,
                                  std::u16string_view rSvgPointsAttribute )
        {
            o_rPoly.clear();
            const sal_Int32 nLen(rSvgPointsAttribute.size());
            sal_Int32 nPos(0);
            double nX, nY;

            // skip initial whitespace
            basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen);

            while(nPos < nLen)
            {
                if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgPointsAttribute, nLen)) return false;
                if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgPointsAttribute, nLen)) return false;

                // add point
                o_rPoly.append(B2DPoint(nX, nY));

                // skip to next number, or finish
                basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen);
            }

            return true;
        }

        OUString exportToSvgPoints( const B2DPolygon& rPoly )
        {
            SAL_WARN_IF(rPoly.areControlPointsUsed(), "basegfx", "exportToSvgPoints: Only non-bezier polygons allowed (!)");
            const sal_uInt32 nPointCount(rPoly.count());
            OUStringBuffer aResult;

            for(sal_uInt32 a(0); a < nPointCount; a++)
            {
                const basegfx::B2DPoint aPoint(rPoly.getB2DPoint(a));

                if(a)
                {
                    aResult.append(' ');
                }

                aResult.append(OUString::number(aPoint.getX())
                        + ","
                        + OUString::number(aPoint.getY()));
            }

            return aResult.makeStringAndClear();
        }

        OUString exportToSvgD(
            const B2DPolyPolygon& rPolyPolygon,
            bool bUseRelativeCoordinates,
            bool bDetectQuadraticBeziers,
            bool bHandleRelativeNextPointCompatible,
            bool bOOXMLMotionPath)
        {
            const sal_uInt32 nCount(rPolyPolygon.count());
            sal_uInt32 nCombinedPointCount = 0;
            for(sal_uInt32 i(0); i < nCount; i++)
            {
                const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i));
                nCombinedPointCount += aPolygon.count();
            }

            OUStringBuffer aResult(std::max<int>(nCombinedPointCount * 32,512));
            B2DPoint aCurrentSVGPosition(0.0, 0.0); // SVG assumes (0,0) as the initial current point

            for(sal_uInt32 i(0); i < nCount; i++)
            {
                const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i));
                const sal_uInt32 nPointCount(aPolygon.count());

                if(nPointCount)
                {
                    const bool bPolyUsesControlPoints(aPolygon.areControlPointsUsed());
                    const sal_uInt32 nEdgeCount(aPolygon.isClosed() ? nPointCount : nPointCount - 1);
                    sal_Unicode aLastSVGCommand(' '); // last SVG command char
                    B2DPoint aLeft, aRight; // for quadratic bezier test

                    // handle polygon start point
                    B2DPoint aEdgeStart(aPolygon.getB2DPoint(0));
                    bool bUseRelativeCoordinatesForFirstPoint(bUseRelativeCoordinates);

                    if(bHandleRelativeNextPointCompatible)
                    {
                        // To get around the error that the start point for the next polygon is the
                        // start point of the current one (and not the last as it was handled up to now)
                        // do force to write an absolute 'M' command as start for the next polygon
                        bUseRelativeCoordinatesForFirstPoint = false;
                    }

                    // Write 'moveto' and the 1st coordinates, set aLastSVGCommand to 'lineto'
                    putCommandChar(aResult, aLastSVGCommand, 'M', bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath);
                    putNumberChar(aResult, aEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath);
                    putNumberChar(aResult, aEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath);
                    aLastSVGCommand =  bUseRelativeCoordinatesForFirstPoint ? 'l' : 'L';
                    aCurrentSVGPosition = aEdgeStart;

                    for(sal_uInt32 nIndex(0); nIndex < nEdgeCount; nIndex++)
                    {
                        // prepare access to next point
                        const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount);
                        const B2DPoint aEdgeEnd(aPolygon.getB2DPoint(nNextIndex));

                        // handle edge from (aEdgeStart, aEdgeEnd) using indices (nIndex, nNextIndex)
                        const bool bEdgeIsBezier(bPolyUsesControlPoints
                            && (aPolygon.isNextControlPointUsed(nIndex) || aPolygon.isPrevControlPointUsed(nNextIndex)));

                        if(bEdgeIsBezier)
                        {
                            // handle bezier edge
                            const B2DPoint aControlEdgeStart(aPolygon.getNextControlPoint(nIndex));
                            const B2DPoint aControlEdgeEnd(aPolygon.getPrevControlPoint(nNextIndex));
                            bool bIsQuadraticBezier(false);

                            // check continuity at current edge's start point. For SVG, do NOT use an
                            // existing continuity since no 'S' or 's' statement should be written. At
                            // import, that 'previous' control vector is not available. SVG documentation
                            // says for interpretation:

                            // "(If there is no previous command or if the previous command was
                            // not a C, c, S or s, assume the first control point is coincident
                            // with the current point.)"

                            // That's what is done from our import, so avoid exporting it as first statement
                            // is necessary.
                            const bool bSymmetricAtEdgeStart(
                                !bOOXMLMotionPath && nIndex != 0
                                && aPolygon.getContinuityInPoint(nIndex) == B2VectorContinuity::C2);

                            if(bDetectQuadraticBeziers)
                            {
                                // check for quadratic beziers - that's
                                // the case if both control points are in
                                // the same place when they are prolonged
                                // to the common quadratic control point

                                // Left: P = (3P1 - P0) / 2
                                // Right: P = (3P2 - P3) / 2
                                aLeft = B2DPoint((3.0 * aControlEdgeStart - aEdgeStart) / 2.0);
                                aRight= B2DPoint((3.0 * aControlEdgeEnd - aEdgeEnd) / 2.0);
                                bIsQuadraticBezier = aLeft.equal(aRight);
                            }

                            if(bIsQuadraticBezier)
                            {
                                // approximately equal, export as quadratic bezier
                                if(bSymmetricAtEdgeStart)
                                {
                                    putCommandChar(aResult, aLastSVGCommand, 'T', bUseRelativeCoordinates, bOOXMLMotionPath);

                                    putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    aCurrentSVGPosition = aEdgeEnd;
                                }
                                else
                                {
                                    putCommandChar(aResult, aLastSVGCommand, 'Q', bUseRelativeCoordinates, bOOXMLMotionPath);

                                    putNumberChar(aResult, aLeft.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aLeft.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    aCurrentSVGPosition = aEdgeEnd;
                                }
                            }
                            else
                            {
                                // export as cubic bezier
                                if(bSymmetricAtEdgeStart)
                                {
                                    putCommandChar(aResult, aLastSVGCommand, 'S', bUseRelativeCoordinates, bOOXMLMotionPath);

                                    putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    aCurrentSVGPosition = aEdgeEnd;
                                }
                                else
                                {
                                    putCommandChar(aResult, aLastSVGCommand, 'C', bUseRelativeCoordinates, bOOXMLMotionPath);

                                    putNumberChar(aResult, aControlEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aControlEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    aCurrentSVGPosition = aEdgeEnd;
                                }
                            }
                        }
                        else
                        {
                            // straight edge
                            if(nNextIndex == 0)
                            {
                                // it's a closed polygon's last edge and it's not a bezier edge, so there is
                                // no need to write it
                            }
                            else
                            {
                                const bool bXEqual(rtl::math::approxEqual(aEdgeStart.getX(), aEdgeEnd.getX()));
                                const bool bYEqual(rtl::math::approxEqual(aEdgeStart.getY(), aEdgeEnd.getY()));

                                if(bXEqual && bYEqual)
                                {
                                    // point is a double point; do not export at all
                                }
                                else if(bXEqual && !bOOXMLMotionPath)
                                {
                                    // export as vertical line
                                    putCommandChar(aResult, aLastSVGCommand, 'V', bUseRelativeCoordinates, bOOXMLMotionPath);

                                    putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    aCurrentSVGPosition = aEdgeEnd;
                                }
                                else if(bYEqual && !bOOXMLMotionPath)
                                {
                                    // export as horizontal line
                                    putCommandChar(aResult, aLastSVGCommand, 'H', bUseRelativeCoordinates, bOOXMLMotionPath);

                                    putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    aCurrentSVGPosition = aEdgeEnd;
                                }
                                else
                                {
                                    // export as line
                                    putCommandChar(aResult, aLastSVGCommand, 'L', bUseRelativeCoordinates, bOOXMLMotionPath);

                                    putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath);
                                    aCurrentSVGPosition = aEdgeEnd;
                                }
                            }
                        }

                        // prepare edge start for next loop step
                        aEdgeStart = aEdgeEnd;
                    }

                    // close path if closed poly (Z and z are equivalent here, but looks nicer when case is matched)
                    if(aPolygon.isClosed())
                    {
                        putCommandChar(aResult, aLastSVGCommand, 'Z', bUseRelativeCoordinates, bOOXMLMotionPath);
                    }
                    else if (bOOXMLMotionPath)
                    {
                        putCommandChar(aResult, aLastSVGCommand, 'E', bUseRelativeCoordinates, bOOXMLMotionPath);
                    }

                    if(!bHandleRelativeNextPointCompatible)
                    {
                        // SVG defines that "the next subpath starts at the same initial point as the current subpath",
                        // so set aCurrentSVGPosition to the 1st point of the current, now ended and written path
                        aCurrentSVGPosition = aPolygon.getB2DPoint(0);
                    }
                }
            }

            return aResult.makeStringAndClear();
        }
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
