/* -*- 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 <drawinglayer/primitive2d/svggradientprimitive2d.hxx>
#include <drawinglayer/primitive2d/drawinglayer_primitivetypes2d.hxx>
#include <drawinglayer/primitive2d/PolyPolygonColorPrimitive2D.hxx>
#include <drawinglayer/primitive2d/unifiedtransparenceprimitive2d.hxx>
#include <basegfx/matrix/b2dhommatrixtools.hxx>
#include <basegfx/polygon/b2dpolygontools.hxx>
#include <basegfx/polygon/b2dpolygon.hxx>
#include <drawinglayer/primitive2d/transparenceprimitive2d.hxx>
#include <drawinglayer/primitive2d/transformprimitive2d.hxx>
#include <drawinglayer/primitive2d/maskprimitive2d.hxx>
#include <drawinglayer/primitive2d/PolyPolygonRGBAPrimitive2D.hxx>
#include <drawinglayer/geometry/viewinformation2d.hxx>
#include <osl/diagnose.h>
#include <sal/log.hxx>
#include <cmath>
#include <utility>
#include <vcl/skia/SkiaHelper.hxx>

using namespace com::sun::star;


namespace
{
    sal_uInt32 calculateStepsForSvgGradient(const basegfx::BColor& rColorA, const basegfx::BColor& rColorB, double fDelta, double fDiscreteUnit)
    {
        // use color distance, assume to do every color step (full quality)
        sal_uInt32 nSteps(basegfx::fround(rColorA.getDistance(rColorB) * 255.0));

        if(nSteps)
        {
            // calc discrete length to change color all 1.5 discrete units (pixels)
            const sal_uInt32 nDistSteps(basegfx::fround(fDelta / (fDiscreteUnit * 1.5)));

            nSteps = std::min(nSteps, nDistSteps);
        }

        // roughly cut when too big or too small
        nSteps = std::min(nSteps, sal_uInt32(255));
        nSteps = std::max(nSteps, sal_uInt32(1));

        return nSteps;
    }
} // end of anonymous namespace


namespace drawinglayer::primitive2d
{
        Primitive2DReference SvgGradientHelper::createSingleGradientEntryFill() const
        {
            const SvgGradientEntryVector& rEntries(getGradientEntries());
            const sal_uInt32 nCount(rEntries.size());

            if(0 == nCount)
            {
                // no entries, done
                OSL_ENSURE(false, "Single gradient entry construction without entry (!)");
                return nullptr;
            }

            const SvgGradientEntry& rSingleEntry(rEntries[nCount - 1]);
            const double fOpacity(rSingleEntry.getOpacity());

            if (fOpacity <= 0.0 || basegfx::fTools::equalZero(fOpacity))
            {
                // completely opaque, done
                return nullptr;
            }

            if (basegfx::fTools::moreOrEqual(fOpacity, 1.0))
            {
                // no opacity
                return Primitive2DReference {
                    new PolyPolygonColorPrimitive2D(
                        getPolyPolygon(),
                        rSingleEntry.getColor()) };
            }

            // if transparent, use PolyPolygonRGBAPrimitive2D
            return Primitive2DReference {
                new PolyPolygonRGBAPrimitive2D(
                    getPolyPolygon(),
                    rSingleEntry.getColor(),
                    1.0 - fOpacity) };
        }

        void SvgGradientHelper::checkPreconditions()
        {
            const SvgGradientEntryVector& rEntries = getGradientEntries();

            if(rEntries.empty())
            {
                // no fill at all, done
                return;
            }

            // sort maGradientEntries by offset, small to big
            std::sort(maGradientEntries.begin(), maGradientEntries.end());

            // gradient with at least two colors
            bool bAllInvisible(true);
            bool bInvalidEntries(false);

            for(const SvgGradientEntry& rCandidate : rEntries)
            {
                if(basegfx::fTools::equalZero(rCandidate.getOpacity()))
                {
                    // invisible
                    mbFullyOpaque = false;
                }
                else if(basegfx::fTools::equal(rCandidate.getOpacity(), 1.0))
                {
                    // completely opaque
                    bAllInvisible = false;
                }
                else
                {
                    // opacity
                    bAllInvisible = false;
                    mbFullyOpaque = false;
                }

                if(!basegfx::fTools::betweenOrEqualEither(rCandidate.getOffset(), 0.0, 1.0))
                {
                    bInvalidEntries = true;
                }
            }

            if(bAllInvisible)
            {
                // all invisible, nothing to do
                return;
            }

            if(bInvalidEntries)
            {
                // invalid entries, do nothing
                SAL_WARN("drawinglayer", "SvgGradientHelper got invalid SvgGradientEntries outside [0.0 .. 1.0]");
                return;
            }

            const basegfx::B2DRange aPolyRange(getPolyPolygon().getB2DRange());

            if(aPolyRange.isEmpty())
            {
                // no range to fill, nothing to do
                return;
            }

            const double fPolyWidth(aPolyRange.getWidth());
            const double fPolyHeight(aPolyRange.getHeight());

            if(basegfx::fTools::equalZero(fPolyWidth) || basegfx::fTools::equalZero(fPolyHeight))
            {
                // no width/height to fill, nothing to do
                return;
            }

            mbCreatesContent = true;

            if(1 == rEntries.size())
            {
                // fill with single existing color
                setSingleEntry();
            }
        }

        const SvgGradientEntry& SvgGradientHelper::FindEntryLessOrEqual(
            sal_Int32& rInt,
            const double fFrac) const
        {
            const bool bMirror(SpreadMethod::Reflect == getSpreadMethod() && 0 != rInt % 2);
            const SvgGradientEntryVector& rCurrent(bMirror ? getMirroredGradientEntries() : getGradientEntries());

            for(SvgGradientEntryVector::const_reverse_iterator aIter(rCurrent.rbegin()); aIter != rCurrent.rend(); ++aIter)
            {
               if(basegfx::fTools::lessOrEqual(aIter->getOffset(), fFrac))
               {
                   return *aIter;
               }
            }

            // walk over gap to the left, be prepared for missing 0.0/1.0 entries
            rInt--;
            const bool bMirror2(SpreadMethod::Reflect == getSpreadMethod() && 0 != rInt % 2);
            const SvgGradientEntryVector& rCurrent2(bMirror2 ? getMirroredGradientEntries() : getGradientEntries());
            return rCurrent2.back();
        }

        const SvgGradientEntry& SvgGradientHelper::FindEntryMore(
            sal_Int32& rInt,
            const double fFrac) const
        {
            const bool bMirror(SpreadMethod::Reflect == getSpreadMethod() && 0 != rInt % 2);
            const SvgGradientEntryVector& rCurrent(bMirror ? getMirroredGradientEntries() : getGradientEntries());

            for(SvgGradientEntryVector::const_iterator aIter(rCurrent.begin()); aIter != rCurrent.end(); ++aIter)
            {
               if(basegfx::fTools::more(aIter->getOffset(), fFrac))
               {
                   return *aIter;
               }
            }

            // walk over gap to the right, be prepared for missing 0.0/1.0 entries
            rInt++;
            const bool bMirror2(SpreadMethod::Reflect == getSpreadMethod() && 0 != rInt % 2);
            const SvgGradientEntryVector& rCurrent2(bMirror2 ? getMirroredGradientEntries() : getGradientEntries());
            return rCurrent2.front();
        }

        // tdf#124424 Adapted creation of color runs to do in a single effort. Previous
        // version tried to do this from [0.0 .. 1.0] and to re-use transformed versions
        // in the caller if SpreadMethod was on some repeat mode, but had problems when
        // e.g. like in the bugdoc from the task a negative-only fStart/fEnd run was
        // requested in which case it did nothing. Even when reusing the spread might
        // not have been a full one from [0.0 .. 1.0].
        // This gets complicated due to mirrored runs, but also for gradient definitions
        // with missing entries for 0.0 and 1.0 in which case these have to be guessed
        // to be there with same parametrisation as their nearest existing entries. These
        // *could* have been added at checkPreconditions() but would then create unnecessary
        // spreads on zone overlaps.
        void SvgGradientHelper::createRun(
            Primitive2DContainer& rTargetColor,
            Primitive2DContainer& rTargetOpacity,
            double fStart,
            double fEnd) const
        {
            double fInt(0.0);
            double fFrac(0.0);
            double fEnd2(0.0);

            if(SpreadMethod::Pad == getSpreadMethod())
            {
                if(fStart < 0.0)
                {
                    fFrac = std::modf(fStart, &fInt);
                    const SvgGradientEntry& rFront(getGradientEntries().front());
                    const SvgGradientEntry aTemp(1.0 + fFrac, rFront.getColor(), rFront.getOpacity());
                    createAtom(rTargetColor, rTargetOpacity, aTemp, rFront, static_cast<sal_Int32>(fInt - 1), 0);
                    fStart = rFront.getOffset();
                }

                if(fEnd > 1.0)
                {
                    // change fEnd early, but create geometry later (after range below)
                    fEnd2 = fEnd;
                    fEnd = getGradientEntries().back().getOffset();
                }
            }

            while(fStart < fEnd)
            {
                fFrac = std::modf(fStart, &fInt);

                if(fFrac < 0.0)
                {
                    fInt -= 1;
                    fFrac = 1.0 + fFrac;
                }

                sal_Int32 nIntLeft(static_cast<sal_Int32>(fInt));
                sal_Int32 nIntRight(nIntLeft);

                const SvgGradientEntry& rLeft(FindEntryLessOrEqual(nIntLeft, fFrac));
                const SvgGradientEntry& rRight(FindEntryMore(nIntRight, fFrac));
                createAtom(rTargetColor, rTargetOpacity, rLeft, rRight, nIntLeft, nIntRight);

                const double fNextfStart(static_cast<double>(nIntRight) + rRight.getOffset());

                if(basegfx::fTools::more(fNextfStart, fStart))
                {
                    fStart = fNextfStart;
                }
                else
                {
                    SAL_WARN("drawinglayer", "SvgGradientHelper spread error");
                    fStart += 1.0;
                }
            }

            if(fEnd2 > 1.0)
            {
                // create end run for SpreadMethod::Pad late to keep correct creation order
                fFrac = std::modf(fEnd2, &fInt);
                const SvgGradientEntry& rBack(getGradientEntries().back());
                const SvgGradientEntry aTemp(fFrac, rBack.getColor(), rBack.getOpacity());
                createAtom(rTargetColor, rTargetOpacity, rBack, aTemp, 0, static_cast<sal_Int32>(fInt));
            }
        }

        Primitive2DReference SvgGradientHelper::createResult(
            Primitive2DContainer aTargetColor,
            Primitive2DContainer aTargetOpacity,
            const basegfx::B2DHomMatrix& rUnitGradientToObject,
            bool bInvert) const
        {
            Primitive2DContainer aTargetColorEntries(aTargetColor.maybeInvert(bInvert));
            Primitive2DContainer aTargetOpacityEntries(aTargetOpacity.maybeInvert(bInvert));

            if(aTargetColorEntries.empty())
                return nullptr;

            Primitive2DReference xRefContent;

            if(!aTargetOpacityEntries.empty())
            {
                const Primitive2DReference xRefOpacity = new TransparencePrimitive2D(
                    std::move(aTargetColorEntries),
                    std::move(aTargetOpacityEntries));

                xRefContent = new TransformPrimitive2D(
                    rUnitGradientToObject,
                    Primitive2DContainer { xRefOpacity });
            }
            else
            {
                xRefContent = new TransformPrimitive2D(
                    rUnitGradientToObject,
                    std::move(aTargetColorEntries));
            }

            return new MaskPrimitive2D(
                getPolyPolygon(),
                Primitive2DContainer { xRefContent });
        }

        SvgGradientHelper::SvgGradientHelper(
            basegfx::B2DHomMatrix aGradientTransform,
            basegfx::B2DPolyPolygon aPolyPolygon,
            SvgGradientEntryVector&& rGradientEntries,
            const basegfx::B2DPoint& rStart,
            bool bUseUnitCoordinates,
            SpreadMethod aSpreadMethod)
        :   maGradientTransform(std::move(aGradientTransform)),
            maPolyPolygon(std::move(aPolyPolygon)),
            maGradientEntries(std::move(rGradientEntries)),
            maStart(rStart),
            maSpreadMethod(aSpreadMethod),
            mbCreatesContent(false),
            mbSingleEntry(false),
            mbFullyOpaque(true),
            mbUseUnitCoordinates(bUseUnitCoordinates)
        {
        }

        SvgGradientHelper::~SvgGradientHelper()
        {
        }

        const SvgGradientEntryVector& SvgGradientHelper::getMirroredGradientEntries() const
        {
            if(maMirroredGradientEntries.empty() && !getGradientEntries().empty())
            {
                const_cast< SvgGradientHelper* >(this)->createMirroredGradientEntries();
            }

            return maMirroredGradientEntries;
        }

        void SvgGradientHelper::createMirroredGradientEntries()
        {
            if(!maMirroredGradientEntries.empty() || getGradientEntries().empty())
                return;

            const sal_uInt32 nCount(getGradientEntries().size());
            maMirroredGradientEntries.clear();
            maMirroredGradientEntries.reserve(nCount);

            for(sal_uInt32 a(0); a < nCount; a++)
            {
                const SvgGradientEntry& rCandidate = getGradientEntries()[nCount - 1 - a];

                maMirroredGradientEntries.emplace_back(
                        1.0 - rCandidate.getOffset(),
                        rCandidate.getColor(),
                        rCandidate.getOpacity());
            }
        }

        bool SvgGradientHelper::operator==(const SvgGradientHelper& rSvgGradientHelper) const
        {
            const SvgGradientHelper& rCompare = rSvgGradientHelper;

            return (getGradientTransform() == rCompare.getGradientTransform()
                && getPolyPolygon() == rCompare.getPolyPolygon()
                && getGradientEntries() == rCompare.getGradientEntries()
                && getStart() == rCompare.getStart()
                && getUseUnitCoordinates() == rCompare.getUseUnitCoordinates()
                && getSpreadMethod() == rCompare.getSpreadMethod());
        }

} // end of namespace drawinglayer::primitive2d


namespace drawinglayer::primitive2d
{
        void SvgLinearGradientPrimitive2D::checkPreconditions()
        {
            // call parent
            SvgGradientHelper::checkPreconditions();

            if(getCreatesContent())
            {
                // Check Vector
                const basegfx::B2DVector aVector(getEnd() - getStart());

                if(basegfx::fTools::equalZero(aVector.getX()) && basegfx::fTools::equalZero(aVector.getY()))
                {
                    // fill with single color using last stop color
                    setSingleEntry();
                }
            }
        }

        void SvgLinearGradientPrimitive2D::createAtom(
            Primitive2DContainer& rTargetColor,
            Primitive2DContainer& rTargetOpacity,
            const SvgGradientEntry& rFrom,
            const SvgGradientEntry& rTo,
            sal_Int32 nOffsetFrom,
            sal_Int32 nOffsetTo) const
        {
            // create gradient atom [rFrom.getOffset() .. rTo.getOffset()] with (rFrom.getOffset() > rTo.getOffset())
            if(rFrom.getOffset() == rTo.getOffset())
            {
                OSL_ENSURE(false, "SvgGradient Atom creation with no step width (!)");
            }
            else
            {
                rTargetColor.push_back(
                    new SvgLinearAtomPrimitive2D(
                        rFrom.getColor(), rFrom.getOffset() + nOffsetFrom,
                        rTo.getColor(), rTo.getOffset() + nOffsetTo));

                if(!getFullyOpaque())
                {
                    const double fTransFrom(1.0 - rFrom.getOpacity());
                    const double fTransTo(1.0 - rTo.getOpacity());
                    const basegfx::BColor aColorFrom(fTransFrom, fTransFrom, fTransFrom);
                    const basegfx::BColor aColorTo(fTransTo, fTransTo, fTransTo);

                    rTargetOpacity.push_back(
                        new SvgLinearAtomPrimitive2D(
                            aColorFrom, rFrom.getOffset() + nOffsetFrom,
                            aColorTo, rTo.getOffset() + nOffsetTo));
                }
            }
        }

        basegfx::B2DHomMatrix SvgLinearGradientPrimitive2D::createUnitGradientToObjectTransformation() const
        {
            const basegfx::B2DRange aPolyRange(getPolyPolygon().getB2DRange());
            const double fPolyWidth(aPolyRange.getWidth());
            const double fPolyHeight(aPolyRange.getHeight());

            // create ObjectTransform based on polygon range
            const basegfx::B2DHomMatrix aObjectTransform(
                basegfx::utils::createScaleTranslateB2DHomMatrix(
                    fPolyWidth, fPolyHeight,
                    aPolyRange.getMinX(), aPolyRange.getMinY()));
            basegfx::B2DHomMatrix aUnitGradientToObject;

            if(getUseUnitCoordinates())
            {
                // interpret in unit coordinate system -> object aspect ratio will scale result
                // create unit transform from unit vector [0.0 .. 1.0] along the X-Axis to given
                // gradient vector defined by Start,End
                const basegfx::B2DVector aVector(getEnd() - getStart());
                const double fVectorLength(aVector.getLength());

                aUnitGradientToObject.scale(fVectorLength, 1.0);
                aUnitGradientToObject.rotate(atan2(aVector.getY(), aVector.getX()));
                aUnitGradientToObject.translate(getStart().getX(), getStart().getY());

                aUnitGradientToObject *= getGradientTransform();

                // create full transform from unit gradient coordinates to object coordinates
                // including the SvgGradient transformation
                aUnitGradientToObject *= aObjectTransform;
            }
            else
            {
                // interpret in object coordinate system -> object aspect ratio will not scale result
                const basegfx::B2DPoint aStart(aObjectTransform * getStart());
                const basegfx::B2DPoint aEnd(aObjectTransform * getEnd());
                const basegfx::B2DVector aVector(aEnd - aStart);

                aUnitGradientToObject.scale(aVector.getLength(), 1.0);
                aUnitGradientToObject.rotate(atan2(aVector.getY(), aVector.getX()));
                aUnitGradientToObject.translate(aStart.getX(), aStart.getY());

                aUnitGradientToObject *= getGradientTransform();
            }

            return aUnitGradientToObject;
        }

        Primitive2DReference SvgLinearGradientPrimitive2D::create2DDecomposition(const geometry::ViewInformation2D& /*rViewInformation*/) const
        {
            if(getSingleEntry())
            {
                // fill with last existing color
                return createSingleGradientEntryFill();
            }
            else if(getCreatesContent())
            {
                // at least two color stops in range [0.0 .. 1.0], sorted, non-null vector, not completely
                // invisible, width and height to fill are not empty
                basegfx::B2DHomMatrix aUnitGradientToObject(createUnitGradientToObjectTransformation());

                // create inverse from it
                basegfx::B2DHomMatrix aObjectToUnitGradient(aUnitGradientToObject);
                aObjectToUnitGradient.invert();

                // back-transform polygon to unit gradient coordinates and get
                // UnitRage. This is the range the gradient has to cover
                basegfx::B2DPolyPolygon aUnitPoly(getPolyPolygon());
                aUnitPoly.transform(aObjectToUnitGradient);
                const basegfx::B2DRange aUnitRange(aUnitPoly.getB2DRange());

                // prepare result vectors
                Primitive2DContainer aTargetColor;
                Primitive2DContainer aTargetOpacity;

                if(aUnitRange.getWidth() > 0.0)
                {
                    // add a pre-multiply to aUnitGradientToObject to allow
                    // multiplication of the polygon(xl, 0.0, xr, 1.0)
                    const basegfx::B2DHomMatrix aPreMultiply(
                        basegfx::utils::createScaleTranslateB2DHomMatrix(
                            1.0, aUnitRange.getHeight(), 0.0, aUnitRange.getMinY()));
                    aUnitGradientToObject = aUnitGradientToObject * aPreMultiply;

                    // create full color run, including all SpreadMethod variants
                    createRun(
                        aTargetColor,
                        aTargetOpacity,
                        aUnitRange.getMinX(),
                        aUnitRange.getMaxX());
                }

                return createResult(std::move(aTargetColor), std::move(aTargetOpacity), aUnitGradientToObject);
            }
            return nullptr;
        }

        SvgLinearGradientPrimitive2D::SvgLinearGradientPrimitive2D(
            const basegfx::B2DHomMatrix& rGradientTransform,
            const basegfx::B2DPolyPolygon& rPolyPolygon,
            SvgGradientEntryVector&& rGradientEntries,
            const basegfx::B2DPoint& rStart,
            const basegfx::B2DPoint& rEnd,
            bool bUseUnitCoordinates,
            SpreadMethod aSpreadMethod)
        :   SvgGradientHelper(rGradientTransform, rPolyPolygon, std::move(rGradientEntries), rStart, bUseUnitCoordinates, aSpreadMethod),
            maEnd(rEnd)
        {
            // ensure Preconditions are checked
            checkPreconditions();
        }

        SvgLinearGradientPrimitive2D::~SvgLinearGradientPrimitive2D()
        {
        }

        bool SvgLinearGradientPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
        {
            const SvgGradientHelper* pSvgGradientHelper = dynamic_cast< const SvgGradientHelper* >(&rPrimitive);

            if(pSvgGradientHelper && SvgGradientHelper::operator==(*pSvgGradientHelper))
            {
                const SvgLinearGradientPrimitive2D& rCompare = static_cast< const SvgLinearGradientPrimitive2D& >(rPrimitive);

                return (getEnd() == rCompare.getEnd());
            }

            return false;
        }

        basegfx::B2DRange SvgLinearGradientPrimitive2D::getB2DRange(const geometry::ViewInformation2D& /*rViewInformation*/) const
        {
            // return ObjectRange
            return getPolyPolygon().getB2DRange();
        }

        // provide unique ID
        sal_uInt32 SvgLinearGradientPrimitive2D::getPrimitive2DID() const
        {
            return PRIMITIVE2D_ID_SVGLINEARGRADIENTPRIMITIVE2D;
        }

} // end of namespace drawinglayer::primitive2d


namespace drawinglayer::primitive2d
{
        void SvgRadialGradientPrimitive2D::checkPreconditions()
        {
            // call parent
            SvgGradientHelper::checkPreconditions();

            if(getCreatesContent())
            {
                // Check Radius
                if(basegfx::fTools::equalZero(getRadius()))
                {
                    // fill with single color using last stop color
                    setSingleEntry();
                }
            }
        }

        void SvgRadialGradientPrimitive2D::createAtom(
            Primitive2DContainer& rTargetColor,
            Primitive2DContainer& rTargetOpacity,
            const SvgGradientEntry& rFrom,
            const SvgGradientEntry& rTo,
            sal_Int32 nOffsetFrom,
            sal_Int32 nOffsetTo) const
        {
            // create gradient atom [rFrom.getOffset() .. rTo.getOffset()] with (rFrom.getOffset() > rTo.getOffset())
            if(rFrom.getOffset() == rTo.getOffset())
            {
                OSL_ENSURE(false, "SvgGradient Atom creation with no step width (!)");
            }
            else
            {
                const double fScaleFrom(rFrom.getOffset() + nOffsetFrom);
                const double fScaleTo(rTo.getOffset() + nOffsetTo);

                if(isFocalSet())
                {
                    const basegfx::B2DVector aFocalVector(getFocal() - getStart());
                    const basegfx::B2DVector aTranslateFrom(aFocalVector * (maFocalLength - fScaleFrom));
                    const basegfx::B2DVector aTranslateTo(aFocalVector * (maFocalLength - fScaleTo));

                    rTargetColor.push_back(
                        new SvgRadialAtomPrimitive2D(
                            rFrom.getColor(), fScaleFrom, aTranslateFrom,
                            rTo.getColor(), fScaleTo, aTranslateTo));
                }
                else
                {
                    rTargetColor.push_back(
                        new SvgRadialAtomPrimitive2D(
                            rFrom.getColor(), fScaleFrom,
                            rTo.getColor(), fScaleTo));
                }

                if(!getFullyOpaque())
                {
                    const double fTransFrom(1.0 - rFrom.getOpacity());
                    const double fTransTo(1.0 - rTo.getOpacity());
                    const basegfx::BColor aColorFrom(fTransFrom, fTransFrom, fTransFrom);
                    const basegfx::BColor aColorTo(fTransTo, fTransTo, fTransTo);

                    if(isFocalSet())
                    {
                        const basegfx::B2DVector aFocalVector(getFocal() - getStart());
                        const basegfx::B2DVector aTranslateFrom(aFocalVector * (maFocalLength - fScaleFrom));
                        const basegfx::B2DVector aTranslateTo(aFocalVector * (maFocalLength - fScaleTo));

                        rTargetOpacity.push_back(
                            new SvgRadialAtomPrimitive2D(
                                aColorFrom, fScaleFrom, aTranslateFrom,
                                aColorTo, fScaleTo, aTranslateTo));
                    }
                    else
                    {
                        rTargetOpacity.push_back(
                            new SvgRadialAtomPrimitive2D(
                                aColorFrom, fScaleFrom,
                                aColorTo, fScaleTo));
                    }
                }
            }
        }

        basegfx::B2DHomMatrix SvgRadialGradientPrimitive2D::createUnitGradientToObjectTransformation() const
        {
            const basegfx::B2DRange aPolyRange(getPolyPolygon().getB2DRange());
            const double fPolyWidth(aPolyRange.getWidth());
            const double fPolyHeight(aPolyRange.getHeight());

            // create ObjectTransform based on polygon range
            const basegfx::B2DHomMatrix aObjectTransform(
                basegfx::utils::createScaleTranslateB2DHomMatrix(
                    fPolyWidth, fPolyHeight,
                    aPolyRange.getMinX(), aPolyRange.getMinY()));
            basegfx::B2DHomMatrix aUnitGradientToObject;

            if(getUseUnitCoordinates())
            {
                // interpret in unit coordinate system -> object aspect ratio will scale result
                // create unit transform from unit vector to given linear gradient vector
                aUnitGradientToObject.scale(getRadius(), getRadius());
                aUnitGradientToObject.translate(getStart().getX(), getStart().getY());

                if(!getGradientTransform().isIdentity())
                {
                    aUnitGradientToObject = getGradientTransform() * aUnitGradientToObject;
                }

                // create full transform from unit gradient coordinates to object coordinates
                // including the SvgGradient transformation
                aUnitGradientToObject = aObjectTransform * aUnitGradientToObject;
            }
            else
            {
                // interpret in object coordinate system -> object aspect ratio will not scale result
                // use X-Axis with radius, it was already made relative to object width when coming from
                // SVG import
                const double fRadius((aObjectTransform * basegfx::B2DVector(getRadius(), 0.0)).getLength());
                const basegfx::B2DPoint aStart(aObjectTransform * getStart());

                aUnitGradientToObject.scale(fRadius, fRadius);
                aUnitGradientToObject.translate(aStart.getX(), aStart.getY());

                aUnitGradientToObject *= getGradientTransform();
            }

            return aUnitGradientToObject;
        }

        Primitive2DReference SvgRadialGradientPrimitive2D::create2DDecomposition(const geometry::ViewInformation2D& /*rViewInformation*/) const
        {
            if(getSingleEntry())
            {
                // fill with last existing color
                return createSingleGradientEntryFill();
            }
            else if(getCreatesContent())
            {
                // at least two color stops in range [0.0 .. 1.0], sorted, non-null vector, not completely
                // invisible, width and height to fill are not empty
                basegfx::B2DHomMatrix aUnitGradientToObject(createUnitGradientToObjectTransformation());

                // create inverse from it
                basegfx::B2DHomMatrix aObjectToUnitGradient(aUnitGradientToObject);
                aObjectToUnitGradient.invert();

                // back-transform polygon to unit gradient coordinates and get
                // UnitRage. This is the range the gradient has to cover
                basegfx::B2DPolyPolygon aUnitPoly(getPolyPolygon());
                aUnitPoly.transform(aObjectToUnitGradient);
                const basegfx::B2DRange aUnitRange(aUnitPoly.getB2DRange());

                // create range which the gradient has to cover to cover the whole given geometry.
                // For circle, go from 0.0 to max radius in all directions (the corners)
                double fMax(basegfx::B2DVector(aUnitRange.getMinimum()).getLength());
                fMax = std::max(fMax, basegfx::B2DVector(aUnitRange.getMaximum()).getLength());
                fMax = std::max(fMax, basegfx::B2DVector(aUnitRange.getMinX(), aUnitRange.getMaxY()).getLength());
                fMax = std::max(fMax, basegfx::B2DVector(aUnitRange.getMaxX(), aUnitRange.getMinY()).getLength());

                // prepare result vectors
                Primitive2DContainer aTargetColor;
                Primitive2DContainer aTargetOpacity;

                if(0.0 < fMax)
                {
                    // prepare maFocalVector
                    if(isFocalSet())
                    {
                        const_cast< SvgRadialGradientPrimitive2D* >(this)->maFocalLength = fMax;
                    }

                    // create full color run, including all SpreadMethod variants
                    createRun(
                        aTargetColor,
                        aTargetOpacity,
                        0.0,
                        fMax);
                }

                return createResult(std::move(aTargetColor), std::move(aTargetOpacity), aUnitGradientToObject, true);
            }
            return nullptr;
        }

        SvgRadialGradientPrimitive2D::SvgRadialGradientPrimitive2D(
            const basegfx::B2DHomMatrix& rGradientTransform,
            const basegfx::B2DPolyPolygon& rPolyPolygon,
            SvgGradientEntryVector&& rGradientEntries,
            const basegfx::B2DPoint& rStart,
            double fRadius,
            bool bUseUnitCoordinates,
            SpreadMethod aSpreadMethod,
            const basegfx::B2DPoint* pFocal)
        :   SvgGradientHelper(rGradientTransform, rPolyPolygon, std::move(rGradientEntries), rStart, bUseUnitCoordinates, aSpreadMethod),
            mfRadius(fRadius),
            maFocal(rStart),
            maFocalLength(0.0)
        {
            if(pFocal && !pFocal->equal(getStart()))
            {
                maFocal = *pFocal;
            }

            // ensure Preconditions are checked
            checkPreconditions();
        }

        SvgRadialGradientPrimitive2D::~SvgRadialGradientPrimitive2D()
        {
        }

        bool SvgRadialGradientPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
        {
            const SvgGradientHelper* pSvgGradientHelper = dynamic_cast< const SvgGradientHelper* >(&rPrimitive);

            if(!pSvgGradientHelper || !SvgGradientHelper::operator==(*pSvgGradientHelper))
                return false;

            const SvgRadialGradientPrimitive2D& rCompare = static_cast< const SvgRadialGradientPrimitive2D& >(rPrimitive);

            if(getRadius() == rCompare.getRadius())
            {
                if(isFocalSet() == rCompare.isFocalSet())
                {
                    if(isFocalSet())
                    {
                        return getFocal() == rCompare.getFocal();
                    }
                    else
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        basegfx::B2DRange SvgRadialGradientPrimitive2D::getB2DRange(const geometry::ViewInformation2D& /*rViewInformation*/) const
        {
            // return ObjectRange
            return getPolyPolygon().getB2DRange();
        }

        // provide unique ID
        sal_uInt32 SvgRadialGradientPrimitive2D::getPrimitive2DID() const
        {
            return PRIMITIVE2D_ID_SVGRADIALGRADIENTPRIMITIVE2D;
        }

} // end of namespace drawinglayer::primitive2d


// SvgLinearAtomPrimitive2D class

namespace drawinglayer::primitive2d
{
        Primitive2DReference SvgLinearAtomPrimitive2D::create2DDecomposition(const geometry::ViewInformation2D& /*rViewInformation*/) const
        {
            const double fDelta(getOffsetB() - getOffsetA());

            if(basegfx::fTools::equalZero(fDelta))
                return nullptr;

            // use one discrete unit for overlap (one pixel)
            const double fDiscreteUnit(getDiscreteUnit());

            // use color distance and discrete lengths to calculate step count
            const sal_uInt32 nSteps(calculateStepsForSvgGradient(getColorA(), getColorB(), fDelta, fDiscreteUnit));

            // HACK: Splitting a gradient into adjacent polygons with gradually changing color is silly.
            // If antialiasing is used to draw them, the AA-ed adjacent edges won't line up perfectly
            // because of the AA (see SkiaSalGraphicsImpl::mergePolyPolygonToPrevious()).
            // Make the polygons a bit wider, so they the partial overlap "fixes" this.
            const double fixup = SkiaHelper::isVCLSkiaEnabled() ? fDiscreteUnit / 2 : 0;

            // tdf#117949 Use a small amount of discrete overlap at the edges. Usually this
            // should be exactly 0.0 and 1.0, but there were cases when this gets clipped
            // against the mask polygon which got numerically problematic.
            // This change is unnecessary in that respect, but avoids that numerical havoc
            // by at the same time doing no real harm AFAIK
            // TTTT: Remove again when clipping is fixed (!)

            // prepare polygon in needed width at start position (with discrete overlap)
            const basegfx::B2DPolygon aPolygon(
                basegfx::utils::createPolygonFromRect(
                    basegfx::B2DRange(
                        getOffsetA() - fDiscreteUnit,
                        -0.0001, // TTTT -> should be 0.0, see comment above
                        getOffsetA() + (fDelta / nSteps) + fDiscreteUnit + fixup,
                        1.0001))); // TTTT -> should be 1.0, see comment above

            // prepare loop (inside to outside, [0.0 .. 1.0[)
            double fUnitScale(0.0);
            const double fUnitStep(1.0 / nSteps);

            Primitive2DContainer aContainer;
            aContainer.resize(nSteps);
            for(sal_uInt32 a(0); a < nSteps; a++, fUnitScale += fUnitStep)
            {
                basegfx::B2DPolygon aNew(aPolygon);

                aNew.transform(basegfx::utils::createTranslateB2DHomMatrix(fDelta * fUnitScale, 0.0));
                aContainer[a] = new PolyPolygonColorPrimitive2D(
                    basegfx::B2DPolyPolygon(aNew),
                    basegfx::interpolate(getColorA(), getColorB(), fUnitScale));
            }
            return new GroupPrimitive2D(std::move(aContainer));
        }

        SvgLinearAtomPrimitive2D::SvgLinearAtomPrimitive2D(
            const basegfx::BColor& aColorA, double fOffsetA,
            const basegfx::BColor& aColorB, double fOffsetB)
        :   maColorA(aColorA),
            maColorB(aColorB),
            mfOffsetA(fOffsetA),
            mfOffsetB(fOffsetB)
        {
            if(mfOffsetA > mfOffsetB)
            {
                OSL_ENSURE(false, "Wrong offset order (!)");
                std::swap(mfOffsetA, mfOffsetB);
            }
        }

        bool SvgLinearAtomPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
        {
            if(DiscreteMetricDependentPrimitive2D::operator==(rPrimitive))
            {
                const SvgLinearAtomPrimitive2D& rCompare = static_cast< const SvgLinearAtomPrimitive2D& >(rPrimitive);

                return (getColorA() == rCompare.getColorA()
                    && getColorB() == rCompare.getColorB()
                    && getOffsetA() == rCompare.getOffsetA()
                    && getOffsetB() == rCompare.getOffsetB());
            }

            return false;
        }

        // provide unique ID
        sal_uInt32 SvgLinearAtomPrimitive2D::getPrimitive2DID() const
        {
            return PRIMITIVE2D_ID_SVGLINEARATOMPRIMITIVE2D;
        }

} // end of namespace drawinglayer::primitive2d


// SvgRadialAtomPrimitive2D class

namespace drawinglayer::primitive2d
{
        Primitive2DReference SvgRadialAtomPrimitive2D::create2DDecomposition(const geometry::ViewInformation2D& /*rViewInformation*/) const
        {
            const double fDeltaScale(getScaleB() - getScaleA());

            if(basegfx::fTools::equalZero(fDeltaScale))
                return nullptr;

            // use one discrete unit for overlap (one pixel)
            const double fDiscreteUnit(getDiscreteUnit());

            // use color distance and discrete lengths to calculate step count
            const sal_uInt32 nSteps(calculateStepsForSvgGradient(getColorA(), getColorB(), fDeltaScale, fDiscreteUnit));

            // prepare loop ([0.0 .. 1.0[, full polygons, no polypolygons with holes)
            double fUnitScale(0.0);
            const double fUnitStep(1.0 / nSteps);

            Primitive2DContainer aContainer;
            aContainer.resize(nSteps);
            for(sal_uInt32 a(0); a < nSteps; a++, fUnitScale += fUnitStep)
            {
                basegfx::B2DHomMatrix aTransform;
                const double fEndScale(getScaleB() - (fDeltaScale * fUnitScale));

                if(isTranslateSet())
                {
                    const basegfx::B2DVector aTranslate(
                        basegfx::interpolate(
                            getTranslateB(),
                            getTranslateA(),
                            fUnitScale));

                    aTransform = basegfx::utils::createScaleTranslateB2DHomMatrix(
                        fEndScale,
                        fEndScale,
                        aTranslate.getX(),
                        aTranslate.getY());
                }
                else
                {
                    aTransform = basegfx::utils::createScaleB2DHomMatrix(
                        fEndScale,
                        fEndScale);
                }

                basegfx::B2DPolygon aNew(basegfx::utils::createPolygonFromUnitCircle());

                aNew.transform(aTransform);
                aContainer[a] = new PolyPolygonColorPrimitive2D(
                    basegfx::B2DPolyPolygon(aNew),
                    basegfx::interpolate(getColorB(), getColorA(), fUnitScale));
            }
            return new GroupPrimitive2D(std::move(aContainer));
        }

        SvgRadialAtomPrimitive2D::SvgRadialAtomPrimitive2D(
            const basegfx::BColor& aColorA, double fScaleA, const basegfx::B2DVector& rTranslateA,
            const basegfx::BColor& aColorB, double fScaleB, const basegfx::B2DVector& rTranslateB)
        :   maColorA(aColorA),
            maColorB(aColorB),
            mfScaleA(fScaleA),
            mfScaleB(fScaleB)
        {
            // check and evtl. set translations
            if(!rTranslateA.equal(rTranslateB))
            {
                mpTranslate.reset( new VectorPair(rTranslateA, rTranslateB) );
            }

            // scale A and B have to be positive
            mfScaleA = std::max(mfScaleA, 0.0);
            mfScaleB = std::max(mfScaleB, 0.0);

            // scale B has to be bigger than scale A; swap if different
            if(mfScaleA > mfScaleB)
            {
                OSL_ENSURE(false, "Wrong offset order (!)");
                std::swap(mfScaleA, mfScaleB);

                if(mpTranslate)
                {
                    std::swap(mpTranslate->maTranslateA, mpTranslate->maTranslateB);
                }
            }
        }

        SvgRadialAtomPrimitive2D::SvgRadialAtomPrimitive2D(
            const basegfx::BColor& aColorA, double fScaleA,
            const basegfx::BColor& aColorB, double fScaleB)
        :   maColorA(aColorA),
            maColorB(aColorB),
            mfScaleA(fScaleA),
            mfScaleB(fScaleB)
        {
            // scale A and B have to be positive
            mfScaleA = std::max(mfScaleA, 0.0);
            mfScaleB = std::max(mfScaleB, 0.0);

            // scale B has to be bigger than scale A; swap if different
            if(mfScaleA > mfScaleB)
            {
                OSL_ENSURE(false, "Wrong offset order (!)");
                std::swap(mfScaleA, mfScaleB);
            }
        }

        SvgRadialAtomPrimitive2D::~SvgRadialAtomPrimitive2D()
        {
        }

        bool SvgRadialAtomPrimitive2D::operator==(const BasePrimitive2D& rPrimitive) const
        {
            if(!DiscreteMetricDependentPrimitive2D::operator==(rPrimitive))
                return false;

            const SvgRadialAtomPrimitive2D& rCompare = static_cast< const SvgRadialAtomPrimitive2D& >(rPrimitive);

            if(getColorA() == rCompare.getColorA()
                && getColorB() == rCompare.getColorB()
                && getScaleA() == rCompare.getScaleA()
                && getScaleB() == rCompare.getScaleB())
            {
                if(isTranslateSet() && rCompare.isTranslateSet())
                {
                    return (getTranslateA() == rCompare.getTranslateA()
                        && getTranslateB() == rCompare.getTranslateB());
                }
                else if(!isTranslateSet() && !rCompare.isTranslateSet())
                {
                    return true;
                }
            }

            return false;
        }

        // provide unique ID
        sal_uInt32 SvgRadialAtomPrimitive2D::getPrimitive2DID() const
        {
            return PRIMITIVE2D_ID_SVGRADIALATOMPRIMITIVE2D;
        }

} // end of namespace

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