/* -*- 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 <sal/config.h>

#include <algorithm>

#include <texture/texture.hxx>
#include <basegfx/numeric/ftools.hxx>
#include <basegfx/utils/gradienttools.hxx>
#include <basegfx/matrix/b2dhommatrixtools.hxx>

#include <comphelper/random.hxx>

namespace drawinglayer::texture
{
        namespace
        {
            double getRandomColorRange()
            {
                return comphelper::rng::uniform_real_distribution(0.0, nextafter(1.0, DBL_MAX));
            }
        }

        GeoTexSvx::GeoTexSvx()
        {
        }

        GeoTexSvx::~GeoTexSvx()
        {
        }

        bool GeoTexSvx::operator==(const GeoTexSvx& /*rGeoTexSvx*/) const
        {
            // default implementation says yes (no data -> no difference)
            return true;
        }

        void GeoTexSvx::modifyBColor(const basegfx::B2DPoint& /*rUV*/, basegfx::BColor& rBColor, double& /*rfOpacity*/) const
        {
            // base implementation creates random color (for testing only, may also be pure virtual)
            rBColor.setRed(getRandomColorRange());
            rBColor.setGreen(getRandomColorRange());
            rBColor.setBlue(getRandomColorRange());
        }

        void GeoTexSvx::modifyOpacity(const basegfx::B2DPoint& rUV, double& rfOpacity) const
        {
            // base implementation uses inverse of luminance of solved color (for testing only, may also be pure virtual)
            basegfx::BColor aBaseColor;
            modifyBColor(rUV, aBaseColor, rfOpacity);
            rfOpacity = 1.0 - aBaseColor.luminance();
        }


        GeoTexSvxGradient::GeoTexSvxGradient(
            const basegfx::B2DRange& rDefinitionRange,
            sal_uInt32 nRequestedSteps,
            const basegfx::BColorStops& rColorStops,
            double fBorder)
        : maDefinitionRange(rDefinitionRange)
        , mnRequestedSteps(nRequestedSteps)
        , mnColorStops(rColorStops)
        , mfBorder(fBorder)
        , maLastColorStopRange()
        {
        }

        GeoTexSvxGradient::~GeoTexSvxGradient()
        {
        }

        bool GeoTexSvxGradient::operator==(const GeoTexSvx& rGeoTexSvx) const
        {
            const GeoTexSvxGradient* pCompare = dynamic_cast< const GeoTexSvxGradient* >(&rGeoTexSvx);

            return (pCompare
                && maGradientInfo == pCompare->maGradientInfo
                && maDefinitionRange == pCompare->maDefinitionRange
                && mnRequestedSteps == pCompare->mnRequestedSteps
                && mnColorStops == pCompare->mnColorStops
                && mfBorder == pCompare->mfBorder);
        }

        GeoTexSvxGradientLinear::GeoTexSvxGradientLinear(
            const basegfx::B2DRange& rDefinitionRange,
            const basegfx::B2DRange& rOutputRange,
            sal_uInt32 nRequestedSteps,
            const basegfx::BColorStops& rColorStops,
            double fBorder,
            double fAngle)
        : GeoTexSvxGradient(rDefinitionRange, nRequestedSteps, rColorStops, fBorder)
        , mfUnitMinX(0.0)
        , mfUnitWidth(1.0)
        , mfUnitMaxY(1.0)
        {
            maGradientInfo = basegfx::utils::createLinearODFGradientInfo(
                rDefinitionRange,
                nRequestedSteps,
                fBorder,
                fAngle);

            if(rDefinitionRange != rOutputRange)
            {
                basegfx::B2DRange aInvOutputRange(rOutputRange);

                aInvOutputRange.transform(maGradientInfo.getBackTextureTransform());
                mfUnitMinX = aInvOutputRange.getMinX();
                mfUnitWidth = aInvOutputRange.getWidth();
                mfUnitMaxY = aInvOutputRange.getMaxY();
            }
        }

        GeoTexSvxGradientLinear::~GeoTexSvxGradientLinear()
        {
        }

        void GeoTexSvxGradientLinear::appendTransformationsAndColors(
            const std::function<void(const basegfx::B2DHomMatrix& rMatrix, const basegfx::BColor& rColor)>& rCallback)
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // only one color, done
            if (mnColorStops.size() < 2)
                return;

            // check if we need last-ColorStop-correction
            const bool bPenultimateUsed(mnColorStops.checkPenultimate());

            if (bPenultimateUsed)
            {
                // Here we need to temporarily add a ColorStop entry with the
                // same color as the last entry to correctly 'close' the
                // created gradient geometry.
                // The simplest way is to temporarily add an entry to the local
                // ColorStops for this at 1.0 (using same color)
                mnColorStops.emplace_back(1.0, mnColorStops.back().getStopColor());
            }

            // prepare unit range transform
            basegfx::B2DHomMatrix aPattern;

            // bring from unit circle [-1, -1, 1, 1] to unit range [0, 0, 1, 1]
            aPattern.scale(0.5, 0.5);
            aPattern.translate(0.5, 0.5);

            // scale and translate in X
            aPattern.scale(mfUnitWidth, 1.0);
            aPattern.translate(mfUnitMinX, 0.0);

            // outer loop over ColorStops, each is from cs_l to cs_r
            for (auto cs_l(mnColorStops.begin()), cs_r(cs_l + 1); cs_r != mnColorStops.end(); cs_l++, cs_r++)
            {
                // get offsets
                const double fOffsetStart(cs_l->getStopOffset());
                const double fOffsetEnd(cs_r->getStopOffset());

                // same offset, empty BColorStopRange, continue with next step
                if (basegfx::fTools::equal(fOffsetStart, fOffsetEnd))
                    continue;

                // get colors & calculate steps
                const basegfx::BColor aCStart(cs_l->getStopColor());
                const basegfx::BColor aCEnd(cs_r->getStopColor());
                const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps(
                    maGradientInfo.getRequestedSteps(), aCStart, aCEnd));

                // calculate StripeWidth
                // nSteps is >= 1, see getRequestedSteps, so no check needed here
                const double fStripeWidth((fOffsetEnd - fOffsetStart) / nSteps);

                // for the 1st color range we do not need to create the 1st step
                // since it will be equal to StartColor and thus OuterColor, so
                // will be painted by the 1st, always-created background polygon
                // colored using OuterColor.
                // We *need* to create this though for all 'inner' color ranges
                // to get a correct start
                const sal_uInt32 nStartInnerLoop(cs_l == mnColorStops.begin() ? 1 : 0);

                for (sal_uInt32 innerLoop(nStartInnerLoop); innerLoop < nSteps; innerLoop++)
                {
                    // calculate pos in Y
                    const double fPos(fOffsetStart + (fStripeWidth * innerLoop));

                    // scale and translate in Y. For GradientLinear we always have
                    // the full height
                    double fHeight(1.0 - fPos);

                    if (mfUnitMaxY > 1.0)
                    {
                        // extend when difference between definition and OutputRange exists
                        fHeight += mfUnitMaxY - 1.0;
                    }

                    basegfx::B2DHomMatrix aNew(aPattern);
                    aNew.scale(1.0, fHeight);
                    aNew.translate(0.0, fPos);

                    // set and add at target
                    rCallback(
                        maGradientInfo.getTextureTransform() * aNew,
                        1 == nSteps ? aCStart : basegfx::BColor(interpolate(aCStart, aCEnd, double(innerLoop) / double(nSteps - 1))));
                }
            }

            if (bPenultimateUsed)
            {
                // correct temporary change
                mnColorStops.pop_back();
            }
        }

        void GeoTexSvxGradientLinear::modifyBColor(const basegfx::B2DPoint& rUV, basegfx::BColor& rBColor, double& /*rfOpacity*/) const
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // just single color, done
            if (mnColorStops.size() < 2)
            {
                rBColor = mnColorStops.front().getStopColor();
                return;
            }

            // texture-back-transform X/Y -> t [0.0..1.0] and determine color
            const double fScaler(basegfx::utils::getLinearGradientAlpha(rUV, maGradientInfo));
            rBColor = mnColorStops.getInterpolatedBColor(fScaler, mnRequestedSteps, maLastColorStopRange);
        }

        GeoTexSvxGradientAxial::GeoTexSvxGradientAxial(
            const basegfx::B2DRange& rDefinitionRange,
            const basegfx::B2DRange& rOutputRange,
            sal_uInt32 nRequestedSteps,
            const basegfx::BColorStops& rColorStops,
            double fBorder,
            double fAngle)
        : GeoTexSvxGradient(rDefinitionRange, nRequestedSteps, rColorStops, fBorder)
        , mfUnitMinX(0.0)
        , mfUnitWidth(1.0)
        {
            // ARGH! GradientAxial uses the ColorStops in reverse order compared
            // with the other gradients. Either stay 'thinking reverse' for the
            // rest of time or adapt it here and go in same order as the other five,
            // so unifications/tooling will be possible
            mnColorStops.reverseColorStops();

            maGradientInfo = basegfx::utils::createAxialODFGradientInfo(
                rDefinitionRange,
                nRequestedSteps,
                fBorder,
                fAngle);

            if(rDefinitionRange != rOutputRange)
            {
                basegfx::B2DRange aInvOutputRange(rOutputRange);

                aInvOutputRange.transform(maGradientInfo.getBackTextureTransform());
                mfUnitMinX = aInvOutputRange.getMinX();
                mfUnitWidth = aInvOutputRange.getWidth();
            }
        }

        GeoTexSvxGradientAxial::~GeoTexSvxGradientAxial()
        {
        }

        void GeoTexSvxGradientAxial::appendTransformationsAndColors(
            const std::function<void(const basegfx::B2DHomMatrix& rMatrix, const basegfx::BColor& rColor)>& rCallback)
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // only one color, done
            if (mnColorStops.size() < 2)
                return;

            // check if we need last-ColorStop-correction
            const bool bPenultimateUsed(mnColorStops.checkPenultimate());

            if (bPenultimateUsed)
            {
                // temporarily add a ColorStop entry
                mnColorStops.emplace_back(1.0, mnColorStops.back().getStopColor());
            }

            // prepare unit range transform
            basegfx::B2DHomMatrix aPattern;

            // bring in X from unit circle [-1, -1, 1, 1] to unit range [0, 0, 1, 1]
            aPattern.scale(0.5, 1.0);
            aPattern.translate(0.5, 0.0);

            // scale/translate in X
            aPattern.scale(mfUnitWidth, 1.0);
            aPattern.translate(mfUnitMinX, 0.0);

            // outer loop over ColorStops, each is from cs_l to cs_r
            for (auto cs_l(mnColorStops.begin()), cs_r(cs_l + 1); cs_r != mnColorStops.end(); cs_l++, cs_r++)
            {
                // get offsets
                const double fOffsetStart(cs_l->getStopOffset());
                const double fOffsetEnd(cs_r->getStopOffset());

                // same offset, empty BColorStopRange, continue with next step
                if (basegfx::fTools::equal(fOffsetStart, fOffsetEnd))
                    continue;

                // get colors & calculate steps
                const basegfx::BColor aCStart(cs_l->getStopColor());
                const basegfx::BColor aCEnd(cs_r->getStopColor());
                const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps(
                    maGradientInfo.getRequestedSteps(), aCStart, aCEnd));

                // calculate StripeWidth
                // nSteps is >= 1, see getRequestedSteps, so no check needed here
                const double fStripeWidth((fOffsetEnd - fOffsetStart) / nSteps);

                // for the 1st color range we do not need to create the 1st step, see above
                const sal_uInt32 nStartInnerLoop(cs_l == mnColorStops.begin() ? 1 : 0);

                for (sal_uInt32 innerLoop(nStartInnerLoop); innerLoop < nSteps; innerLoop++)
                {
                    // calculate pos in Y
                    const double fPos(fOffsetStart + (fStripeWidth * innerLoop));

                    // already centered in Y on X-Axis, just scale in Y
                    basegfx::B2DHomMatrix aNew(aPattern);
                    aNew.scale(1.0, 1.0 - fPos);

                    // set and add at target
                    rCallback(
                        maGradientInfo.getTextureTransform() * aNew,
                        1 == nSteps ? aCStart : basegfx::BColor(interpolate(aCStart, aCEnd, double(innerLoop) / double(nSteps - 1))));
                }
            }

            if (bPenultimateUsed)
            {
                // correct temporary change
                mnColorStops.pop_back();
            }
        }

        void GeoTexSvxGradientAxial::modifyBColor(const basegfx::B2DPoint& rUV, basegfx::BColor& rBColor, double& /*rfOpacity*/) const
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // just single color, done
            if (mnColorStops.size() < 2)
            {
                // we use the reverse ColorSteps here, so use front value
                rBColor = mnColorStops.front().getStopColor();
                return;
            }

            // texture-back-transform X/Y -> t [0.0..1.0] and determine color
            const double fScaler(basegfx::utils::getAxialGradientAlpha(rUV, maGradientInfo));

                // we use the reverse ColorSteps here, so mirror scaler value
            rBColor = mnColorStops.getInterpolatedBColor(1.0 - fScaler, mnRequestedSteps, maLastColorStopRange);
        }


        GeoTexSvxGradientRadial::GeoTexSvxGradientRadial(
            const basegfx::B2DRange& rDefinitionRange,
            sal_uInt32 nRequestedSteps,
            const basegfx::BColorStops& rColorStops,
            double fBorder,
            double fOffsetX,
            double fOffsetY)
        : GeoTexSvxGradient(rDefinitionRange, nRequestedSteps, rColorStops, fBorder)
        {
            maGradientInfo = basegfx::utils::createRadialODFGradientInfo(
                rDefinitionRange,
                basegfx::B2DVector(fOffsetX,fOffsetY),
                nRequestedSteps,
                fBorder);
        }

        GeoTexSvxGradientRadial::~GeoTexSvxGradientRadial()
        {
        }

        void GeoTexSvxGradientRadial::appendTransformationsAndColors(
            const std::function<void(const basegfx::B2DHomMatrix& rMatrix, const basegfx::BColor& rColor)>& rCallback)
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // only one color, done
            if (mnColorStops.size() < 2)
                return;

            // check if we need last-ColorStop-correction
            const bool bPenultimateUsed(mnColorStops.checkPenultimate());

            if (bPenultimateUsed)
            {
                // temporarily add a ColorStop entry
                mnColorStops.emplace_back(1.0, mnColorStops.back().getStopColor());
            }

            // outer loop over ColorStops, each is from cs_l to cs_r
            for (auto cs_l(mnColorStops.begin()), cs_r(cs_l + 1); cs_r != mnColorStops.end(); cs_l++, cs_r++)
            {
                // get offsets
                const double fOffsetStart(cs_l->getStopOffset());
                const double fOffsetEnd(cs_r->getStopOffset());

                // same offset, empty BColorStopRange, continue with next step
                if (basegfx::fTools::equal(fOffsetStart, fOffsetEnd))
                    continue;

                // get colors & calculate steps
                const basegfx::BColor aCStart(cs_l->getStopColor());
                const basegfx::BColor aCEnd(cs_r->getStopColor());
                const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps(
                    maGradientInfo.getRequestedSteps(), aCStart, aCEnd));

                // calculate StripeWidth
                const double fStripeWidth((fOffsetEnd - fOffsetStart) / nSteps);

                // get correct start for inner loop (see above)
                const sal_uInt32 nStartInnerLoop(cs_l == mnColorStops.begin() ? 1 : 0);

                for (sal_uInt32 innerLoop(nStartInnerLoop); innerLoop < nSteps; innerLoop++)
                {
                    // calculate size/radius
                    const double fSize(1.0 - (fOffsetStart + (fStripeWidth * innerLoop)));

                    // set and add at target
                    rCallback(
                        maGradientInfo.getTextureTransform() * basegfx::utils::createScaleB2DHomMatrix(fSize, fSize),
                        1 == nSteps ? aCStart : basegfx::BColor(interpolate(aCStart, aCEnd, double(innerLoop) / double(nSteps - 1))));
                }
            }

            if (bPenultimateUsed)
            {
                // correct temporary change
                mnColorStops.pop_back();
            }
        }

        void GeoTexSvxGradientRadial::modifyBColor(const basegfx::B2DPoint& rUV, basegfx::BColor& rBColor, double& /*rfOpacity*/) const
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // just single color, done
            if (mnColorStops.size() < 2)
            {
                rBColor = mnColorStops.front().getStopColor();
                return;
            }

            // texture-back-transform X/Y -> t [0.0..1.0] and determine color
            const double fScaler(basegfx::utils::getRadialGradientAlpha(rUV, maGradientInfo));
            rBColor = mnColorStops.getInterpolatedBColor(fScaler, mnRequestedSteps, maLastColorStopRange);
        }


        GeoTexSvxGradientElliptical::GeoTexSvxGradientElliptical(
            const basegfx::B2DRange& rDefinitionRange,
            sal_uInt32 nRequestedSteps,
            const basegfx::BColorStops& rColorStops,
            double fBorder,
            double fOffsetX,
            double fOffsetY,
            double fAngle)
        : GeoTexSvxGradient(rDefinitionRange, nRequestedSteps, rColorStops, fBorder)
        {
            maGradientInfo = basegfx::utils::createEllipticalODFGradientInfo(
                rDefinitionRange,
                basegfx::B2DVector(fOffsetX,fOffsetY),
                nRequestedSteps,
                fBorder,
                fAngle);
        }

        GeoTexSvxGradientElliptical::~GeoTexSvxGradientElliptical()
        {
        }

        void GeoTexSvxGradientElliptical::appendTransformationsAndColors(
            const std::function<void(const basegfx::B2DHomMatrix& rMatrix, const basegfx::BColor& rColor)>& rCallback)
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // only one color, done
            if (mnColorStops.size() < 2)
                return;

            // check if we need last-ColorStop-correction
            const bool bPenultimateUsed(mnColorStops.checkPenultimate());

            if (bPenultimateUsed)
            {
                // temporarily add a ColorStop entry
                mnColorStops.emplace_back(1.0, mnColorStops.back().getStopColor());
            }

            // prepare vars dependent on aspect ratio
            const double fAR(maGradientInfo.getAspectRatio());
            const bool bMTO(fAR > 1.0);

            // outer loop over ColorStops, each is from cs_l to cs_r
            for (auto cs_l(mnColorStops.begin()), cs_r(cs_l + 1); cs_r != mnColorStops.end(); cs_l++, cs_r++)
            {
                // get offsets
                const double fOffsetStart(cs_l->getStopOffset());
                const double fOffsetEnd(cs_r->getStopOffset());

                // same offset, empty BColorStopRange, continue with next step
                if (basegfx::fTools::equal(fOffsetStart, fOffsetEnd))
                    continue;

                // get colors & calculate steps
                const basegfx::BColor aCStart(cs_l->getStopColor());
                const basegfx::BColor aCEnd(cs_r->getStopColor());
                const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps(
                    maGradientInfo.getRequestedSteps(), aCStart, aCEnd));

                // calculate StripeWidth
                const double fStripeWidth((fOffsetEnd - fOffsetStart) / nSteps);

                // get correct start for inner loop (see above)
                const sal_uInt32 nStartInnerLoop(cs_l == mnColorStops.begin() ? 1 : 0);

                for (sal_uInt32 innerLoop(nStartInnerLoop); innerLoop < nSteps; innerLoop++)
                {
                    // calculate offset position for entry
                    const double fSize(fOffsetStart + (fStripeWidth * innerLoop));

                    // set and add at target
                    rCallback(
                        maGradientInfo.getTextureTransform()
                        * basegfx::utils::createScaleB2DHomMatrix(
                            1.0 - (bMTO ? fSize / fAR : fSize),
                            1.0 - (bMTO ? fSize : fSize * fAR)),
                        1 == nSteps ? aCStart : basegfx::BColor(interpolate(aCStart, aCEnd, double(innerLoop) / double(nSteps - 1))));
                }
            }

            if (bPenultimateUsed)
            {
                // correct temporary change
                mnColorStops.pop_back();
            }
        }

        void GeoTexSvxGradientElliptical::modifyBColor(const basegfx::B2DPoint& rUV, basegfx::BColor& rBColor, double& /*rfOpacity*/) const
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // just single color, done
            if (mnColorStops.size() < 2)
            {
                rBColor = mnColorStops.front().getStopColor();
                return;
            }

            // texture-back-transform X/Y -> t [0.0..1.0] and determine color
            const double fScaler(basegfx::utils::getEllipticalGradientAlpha(rUV, maGradientInfo));
            rBColor = mnColorStops.getInterpolatedBColor(fScaler, mnRequestedSteps, maLastColorStopRange);
        }


        GeoTexSvxGradientSquare::GeoTexSvxGradientSquare(
            const basegfx::B2DRange& rDefinitionRange,
            sal_uInt32 nRequestedSteps,
            const basegfx::BColorStops& rColorStops,
            double fBorder,
            double fOffsetX,
            double fOffsetY,
            double fAngle)
        : GeoTexSvxGradient(rDefinitionRange, nRequestedSteps, rColorStops, fBorder)
        {
            maGradientInfo = basegfx::utils::createSquareODFGradientInfo(
                rDefinitionRange,
                basegfx::B2DVector(fOffsetX,fOffsetY),
                nRequestedSteps,
                fBorder,
                fAngle);
        }

        GeoTexSvxGradientSquare::~GeoTexSvxGradientSquare()
        {
        }

        void GeoTexSvxGradientSquare::appendTransformationsAndColors(
            const std::function<void(const basegfx::B2DHomMatrix& rMatrix, const basegfx::BColor& rColor)>& rCallback)
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // only one color, done
            if (mnColorStops.size() < 2)
                return;

            // check if we need last-ColorStop-correction
            const bool bPenultimateUsed(mnColorStops.checkPenultimate());

            if (bPenultimateUsed)
            {
                // temporarily add a ColorStop entry
                mnColorStops.emplace_back(1.0, mnColorStops.back().getStopColor());
            }

            // outer loop over ColorStops, each is from cs_l to cs_r
            for (auto cs_l(mnColorStops.begin()), cs_r(cs_l + 1); cs_r != mnColorStops.end(); cs_l++, cs_r++)
            {
                // get offsets
                const double fOffsetStart(cs_l->getStopOffset());
                const double fOffsetEnd(cs_r->getStopOffset());

                // same offset, empty BColorStopRange, continue with next step
                if (basegfx::fTools::equal(fOffsetStart, fOffsetEnd))
                    continue;

                // get colors & calculate steps
                const basegfx::BColor aCStart(cs_l->getStopColor());
                const basegfx::BColor aCEnd(cs_r->getStopColor());
                const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps(
                    maGradientInfo.getRequestedSteps(), aCStart, aCEnd));

                // calculate StripeWidth
                const double fStripeWidth((fOffsetEnd - fOffsetStart) / nSteps);

                // get correct start for inner loop (see above)
                const sal_uInt32 nStartInnerLoop(cs_l == mnColorStops.begin() ? 1 : 0);

                for (sal_uInt32 innerLoop(nStartInnerLoop); innerLoop < nSteps; innerLoop++)
                {
                    // calculate size/radius
                    const double fSize(1.0 - (fOffsetStart + (fStripeWidth * innerLoop)));

                    // set and add at target
                    rCallback(
                        maGradientInfo.getTextureTransform() * basegfx::utils::createScaleB2DHomMatrix(fSize, fSize),
                        1 == nSteps ? aCStart : basegfx::BColor(interpolate(aCStart, aCEnd, double(innerLoop) / double(nSteps - 1))));
                }
            }

            if (bPenultimateUsed)
            {
                // correct temporary change
                mnColorStops.pop_back();
            }
        }

        void GeoTexSvxGradientSquare::modifyBColor(const basegfx::B2DPoint& rUV, basegfx::BColor& rBColor, double& /*rfOpacity*/) const
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // just single color, done
            if (mnColorStops.size() < 2)
            {
                rBColor = mnColorStops.front().getStopColor();
                return;
            }

            // texture-back-transform X/Y -> t [0.0..1.0] and determine color
            const double fScaler(basegfx::utils::getSquareGradientAlpha(rUV, maGradientInfo));
            rBColor = mnColorStops.getInterpolatedBColor(fScaler, mnRequestedSteps, maLastColorStopRange);
        }


        GeoTexSvxGradientRect::GeoTexSvxGradientRect(
            const basegfx::B2DRange& rDefinitionRange,
            sal_uInt32 nRequestedSteps,
            const basegfx::BColorStops& rColorStops,
            double fBorder,
            double fOffsetX,
            double fOffsetY,
            double fAngle)
        : GeoTexSvxGradient(rDefinitionRange, nRequestedSteps, rColorStops, fBorder)
        {
            maGradientInfo = basegfx::utils::createRectangularODFGradientInfo(
                rDefinitionRange,
                basegfx::B2DVector(fOffsetX,fOffsetY),
                nRequestedSteps,
                fBorder,
                fAngle);
        }

        GeoTexSvxGradientRect::~GeoTexSvxGradientRect()
        {
        }

        void GeoTexSvxGradientRect::appendTransformationsAndColors(
            const std::function<void(const basegfx::B2DHomMatrix& rMatrix, const basegfx::BColor& rColor)>& rCallback)
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // only one color, done
            if (mnColorStops.size() < 2)
                return;

            // check if we need last-ColorStop-correction
            const bool bPenultimateUsed(mnColorStops.checkPenultimate());

            if (bPenultimateUsed)
            {
                // temporarily add a ColorStop entry
                mnColorStops.emplace_back(1.0, mnColorStops.back().getStopColor());
            }

            // prepare vars dependent on aspect ratio
            const double fAR(maGradientInfo.getAspectRatio());
            const bool bMTO(fAR > 1.0);

            // outer loop over ColorStops, each is from cs_l to cs_r
            for (auto cs_l(mnColorStops.begin()), cs_r(cs_l + 1); cs_r != mnColorStops.end(); cs_l++, cs_r++)
            {
                // get offsets
                const double fOffsetStart(cs_l->getStopOffset());
                const double fOffsetEnd(cs_r->getStopOffset());

                // same offset, empty BColorStopRange, continue with next step
                if (basegfx::fTools::equal(fOffsetStart, fOffsetEnd))
                    continue;

                // get colors & calculate steps
                const basegfx::BColor aCStart(cs_l->getStopColor());
                const basegfx::BColor aCEnd(cs_r->getStopColor());
                const sal_uInt32 nSteps(basegfx::utils::calculateNumberOfSteps(
                    maGradientInfo.getRequestedSteps(), aCStart, aCEnd));

                // calculate StripeWidth
                const double fStripeWidth((fOffsetEnd - fOffsetStart) / nSteps);

                // get correct start for inner loop (see above)
                const sal_uInt32 nStartInnerLoop(cs_l == mnColorStops.begin() ? 1 : 0);

                for (sal_uInt32 innerLoop(nStartInnerLoop); innerLoop < nSteps; innerLoop++)
                {
                    // calculate offset position for entry
                    const double fSize(fOffsetStart + (fStripeWidth * innerLoop));

                    // set and add at target
                    rCallback(
                        maGradientInfo.getTextureTransform()
                        * basegfx::utils::createScaleB2DHomMatrix(
                            1.0 - (bMTO ? fSize / fAR : fSize),
                            1.0 - (bMTO ? fSize : fSize * fAR)),
                        1 == nSteps ? aCStart : basegfx::BColor(interpolate(aCStart, aCEnd, double(innerLoop) / double(nSteps - 1))));
                }
            }

            if (bPenultimateUsed)
            {
                // correct temporary change
                mnColorStops.pop_back();
            }
        }

        void GeoTexSvxGradientRect::modifyBColor(const basegfx::B2DPoint& rUV, basegfx::BColor& rBColor, double& /*rfOpacity*/) const
        {
            // no color at all, done
            if (mnColorStops.empty())
                return;

            // just single color, done
            if (mnColorStops.size() < 2)
            {
                rBColor = mnColorStops.front().getStopColor();
                return;
            }

            // texture-back-transform X/Y -> t [0.0..1.0] and determine color
            const double fScaler(basegfx::utils::getRectangularGradientAlpha(rUV, maGradientInfo));
            rBColor = mnColorStops.getInterpolatedBColor(fScaler, mnRequestedSteps, maLastColorStopRange);
        }


        GeoTexSvxHatch::GeoTexSvxHatch(
            const basegfx::B2DRange& rDefinitionRange,
            const basegfx::B2DRange& rOutputRange,
            double fDistance,
            double fAngle)
        :   maOutputRange(rOutputRange),
            mfDistance(0.1),
            mfAngle(fAngle),
            mnSteps(10),
            mbDefinitionRangeEqualsOutputRange(rDefinitionRange == rOutputRange)
        {
            double fTargetSizeX(rDefinitionRange.getWidth());
            double fTargetSizeY(rDefinitionRange.getHeight());
            double fTargetOffsetX(rDefinitionRange.getMinX());
            double fTargetOffsetY(rDefinitionRange.getMinY());

            fAngle = -fAngle;

            // add object expansion
            if(0.0 != fAngle)
            {
                const double fAbsCos(fabs(cos(fAngle)));
                const double fAbsSin(fabs(sin(fAngle)));
                const double fNewX(fTargetSizeX * fAbsCos + fTargetSizeY * fAbsSin);
                const double fNewY(fTargetSizeY * fAbsCos + fTargetSizeX * fAbsSin);
                fTargetOffsetX -= (fNewX - fTargetSizeX) / 2.0;
                fTargetOffsetY -= (fNewY - fTargetSizeY) / 2.0;
                fTargetSizeX = fNewX;
                fTargetSizeY = fNewY;
            }

            // add object scale before rotate
            maTextureTransform.scale(fTargetSizeX, fTargetSizeY);

            // add texture rotate after scale to keep perpendicular angles
            if(0.0 != fAngle)
            {
                basegfx::B2DPoint aCenter(0.5, 0.5);
                aCenter *= maTextureTransform;

                maTextureTransform = basegfx::utils::createRotateAroundPoint(aCenter, fAngle)
                    * maTextureTransform;
            }

            // add object translate
            maTextureTransform.translate(fTargetOffsetX, fTargetOffsetY);

            // prepare height for texture
            const double fSteps((0.0 != fDistance) ? fTargetSizeY / fDistance : 10.0);
            mnSteps = basegfx::fround(fSteps + 0.5);
            mfDistance = 1.0 / fSteps;
        }

        GeoTexSvxHatch::~GeoTexSvxHatch()
        {
        }

        bool GeoTexSvxHatch::operator==(const GeoTexSvx& rGeoTexSvx) const
        {
            const GeoTexSvxHatch* pCompare = dynamic_cast< const GeoTexSvxHatch* >(&rGeoTexSvx);
            return (pCompare
                && maOutputRange == pCompare->maOutputRange
                && maTextureTransform == pCompare->maTextureTransform
                && mfDistance == pCompare->mfDistance
                && mfAngle == pCompare->mfAngle
                && mnSteps == pCompare->mnSteps);
        }

        void GeoTexSvxHatch::appendTransformations(std::vector< basegfx::B2DHomMatrix >& rMatrices)
        {
            if(mbDefinitionRangeEqualsOutputRange)
            {
                // simple hatch where the definition area equals the output area
                for(sal_uInt32 a(1); a < mnSteps; a++)
                {
                    // create matrix
                    const double fOffset(mfDistance * static_cast<double>(a));
                    basegfx::B2DHomMatrix aNew;
                    aNew.set(1, 2, fOffset);
                    rMatrices.push_back(maTextureTransform * aNew);
                }
            }
            else
            {
                // output area is different from definition area, back-transform to get
                // the output area in unit coordinates and fill this with hatch lines
                // using the settings derived from the definition area
                basegfx::B2DRange aBackUnitRange(maOutputRange);

                aBackUnitRange.transform(getBackTextureTransform());

                // calculate vertical start value and a security maximum integer value to avoid death loops
                double fStart(basegfx::snapToNearestMultiple(aBackUnitRange.getMinY(), mfDistance));
                const sal_uInt32 nNeededIntegerSteps(basegfx::fround((aBackUnitRange.getHeight() / mfDistance) + 0.5));
                sal_uInt32 nMaxIntegerSteps(std::min(nNeededIntegerSteps, sal_uInt32(10000)));

                while(fStart < aBackUnitRange.getMaxY() && nMaxIntegerSteps)
                {
                    // create new transform for
                    basegfx::B2DHomMatrix aNew;

                    // adapt x scale and position
                    //aNew.scale(aBackUnitRange.getWidth(), 1.0);
                    //aNew.translate(aBackUnitRange.getMinX(), 0.0);
                    aNew.set(0, 0, aBackUnitRange.getWidth());
                    aNew.set(0, 2, aBackUnitRange.getMinX());

                    // adapt y position to current step
                    aNew.set(1, 2, fStart);
                    //aNew.translate(0.0, fStart);

                    // add new transformation
                    rMatrices.push_back(maTextureTransform * aNew);

                    // next step
                    fStart += mfDistance;
                    nMaxIntegerSteps--;
                }
            }
        }

        double GeoTexSvxHatch::getDistanceToHatch(const basegfx::B2DPoint& rUV) const
        {
            // the below is an inlined and optimised version of
            //     const basegfx::B2DPoint aCoor(getBackTextureTransform() * rUV);
            //     return fmod(aCoor.getY(), mfDistance);

            const basegfx::B2DHomMatrix& rMat = getBackTextureTransform();
            double fX = rUV.getX();
            double fY = rUV.getY();

            double fTempY(
                rMat.get(1, 0) * fX +
                rMat.get(1, 1) * fY +
                rMat.get(1, 2));

            return fmod(fTempY, mfDistance);
        }

        const basegfx::B2DHomMatrix& GeoTexSvxHatch::getBackTextureTransform() const
        {
            if(maBackTextureTransform.isIdentity())
            {
                const_cast< GeoTexSvxHatch* >(this)->maBackTextureTransform = maTextureTransform;
                const_cast< GeoTexSvxHatch* >(this)->maBackTextureTransform.invert();
            }

            return maBackTextureTransform;
        }


        GeoTexSvxTiled::GeoTexSvxTiled(
            const basegfx::B2DRange& rRange,
            double fOffsetX,
            double fOffsetY)
        :   maRange(rRange),
            mfOffsetX(std::clamp(fOffsetX, 0.0, 1.0)),
            mfOffsetY(std::clamp(fOffsetY, 0.0, 1.0))
        {
            if(!basegfx::fTools::equalZero(mfOffsetX))
            {
                mfOffsetY = 0.0;
            }
        }

        GeoTexSvxTiled::~GeoTexSvxTiled()
        {
        }

        bool GeoTexSvxTiled::operator==(const GeoTexSvx& rGeoTexSvx) const
        {
            const GeoTexSvxTiled* pCompare = dynamic_cast< const GeoTexSvxTiled* >(&rGeoTexSvx);

            return (pCompare
                && maRange == pCompare->maRange
                && mfOffsetX == pCompare->mfOffsetX
                && mfOffsetY == pCompare->mfOffsetY);
        }

        sal_uInt32 GeoTexSvxTiled::getNumberOfTiles() const
        {
            sal_Int32 nTiles = 0;
            iterateTiles([&](double, double) { ++nTiles; });
            return nTiles;
        }

        void GeoTexSvxTiled::appendTransformations(std::vector< basegfx::B2DHomMatrix >& rMatrices) const
        {
            const double fWidth(maRange.getWidth());
            const double fHeight(maRange.getHeight());
            iterateTiles([&](double fPosX, double fPosY) {
                rMatrices.push_back(basegfx::utils::createScaleTranslateB2DHomMatrix(
                                        fWidth,
                                        fHeight,
                                        fPosX,
                                        fPosY));
                });
        }

        void GeoTexSvxTiled::iterateTiles(std::function<void(double fPosX, double fPosY)> aFunc) const
        {
            const double fWidth(maRange.getWidth());

            if(basegfx::fTools::equalZero(fWidth))
                return;

            const double fHeight(maRange.getHeight());

            if(basegfx::fTools::equalZero(fHeight))
                return;

            double fStartX(maRange.getMinX());
            double fStartY(maRange.getMinY());
            sal_Int32 nPosX(0);
            sal_Int32 nPosY(0);

            if(fStartX > 0.0)
            {
                const sal_Int32 nDiff(static_cast<sal_Int32>(floor(fStartX / fWidth)) + 1);

                nPosX -= nDiff;
                fStartX -= nDiff * fWidth;
            }

            if((fStartX + fWidth) < 0.0)
            {
                const sal_Int32 nDiff(static_cast<sal_Int32>(floor(-fStartX / fWidth)));

                nPosX += nDiff;
                fStartX += nDiff * fWidth;
            }

            if(fStartY > 0.0)
            {
                const sal_Int32 nDiff(static_cast<sal_Int32>(floor(fStartY / fHeight)) + 1);

                nPosY -= nDiff;
                fStartY -= nDiff * fHeight;
            }

            if((fStartY + fHeight) < 0.0)
            {
                const sal_Int32 nDiff(static_cast<sal_Int32>(floor(-fStartY / fHeight)));

                nPosY += nDiff;
                fStartY += nDiff * fHeight;
            }

            if(!basegfx::fTools::equalZero(mfOffsetY))
            {
                for(double fPosX(fStartX); basegfx::fTools::less(fPosX, 1.0); fPosX += fWidth, nPosX++)
                {
                    for(double fPosY((nPosX % 2) ? fStartY - fHeight + (mfOffsetY * fHeight) : fStartY);
                        basegfx::fTools::less(fPosY, 1.0); fPosY += fHeight)
                        aFunc(fPosX, fPosY);
                }
            }
            else
            {
                for(double fPosY(fStartY); basegfx::fTools::less(fPosY, 1.0); fPosY += fHeight, nPosY++)
                {
                    for(double fPosX((nPosY % 2) ? fStartX - fWidth + (mfOffsetX * fWidth) : fStartX);
                        basegfx::fTools::less(fPosX, 1.0); fPosX += fWidth)
                        aFunc(fPosX, fPosY);
                }
            }

        }

} // end of namespace

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