/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the Collabora Office 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 <sal/log.hxx>

#include <cassert>
#include <cstring>
#include <numeric>
#include <utility>

#include <basegfx/polygon/b2dpolygon.hxx>
#include <basegfx/polygon/b2dpolygontools.hxx>
#include <basegfx/polygon/b2dpolypolygontools.hxx>
#include <osl/endian.h>
#include <osl/file.hxx>
#include <sal/types.h>
#include <tools/long.hxx>
#include <vcl/sysdata.hxx>

#include <fontsubset.hxx>
#include <quartz/salbmp.h>
#ifdef MACOSX
#include <quartz/salgdi.h>
#endif
#include <quartz/utils.h>
#ifdef IOS
#include <ios/iosinst.hxx>
#endif

using namespace vcl;

namespace
{
const basegfx::B2DPoint aHalfPointOfs(0.5, 0.5);

void AddPolygonToPath(CGMutablePathRef xPath, const basegfx::B2DPolygon& rPolygon, bool bClosePath,
                      bool bPixelSnap, bool bLineDraw)
{
    // short circuit if there is nothing to do
    const int nPointCount = rPolygon.count();
    if (nPointCount <= 0)
    {
        return;
    }

    const bool bHasCurves = rPolygon.areControlPointsUsed();
    for (int nPointIdx = 0, nPrevIdx = 0;; nPrevIdx = nPointIdx++)
    {
        int nClosedIdx = nPointIdx;
        if (nPointIdx >= nPointCount)
        {
            // prepare to close last curve segment if needed
            if (bClosePath && (nPointIdx == nPointCount))
            {
                nClosedIdx = 0;
            }
            else
            {
                break;
            }
        }

        basegfx::B2DPoint aPoint = rPolygon.getB2DPoint(nClosedIdx);

        if (bPixelSnap)
        {
            // snap device coordinates to full pixels
            aPoint.setX(basegfx::fround(aPoint.getX()));
            aPoint.setY(basegfx::fround(aPoint.getY()));
        }

        if (bLineDraw)
        {
            aPoint += aHalfPointOfs;
        }
        if (!nPointIdx)
        {
            // first point => just move there
            CGPathMoveToPoint(xPath, nullptr, aPoint.getX(), aPoint.getY());
            continue;
        }

        bool bPendingCurve = false;
        if (bHasCurves)
        {
            bPendingCurve = rPolygon.isNextControlPointUsed(nPrevIdx);
            bPendingCurve |= rPolygon.isPrevControlPointUsed(nClosedIdx);
        }

        if (!bPendingCurve) // line segment
        {
            CGPathAddLineToPoint(xPath, nullptr, aPoint.getX(), aPoint.getY());
        }
        else // cubic bezier segment
        {
            basegfx::B2DPoint aCP1 = rPolygon.getNextControlPoint(nPrevIdx);
            basegfx::B2DPoint aCP2 = rPolygon.getPrevControlPoint(nClosedIdx);
            if (bLineDraw)
            {
                aCP1 += aHalfPointOfs;
                aCP2 += aHalfPointOfs;
            }
            CGPathAddCurveToPoint(xPath, nullptr, aCP1.getX(), aCP1.getY(), aCP2.getX(),
                                  aCP2.getY(), aPoint.getX(), aPoint.getY());
        }
    }

    if (bClosePath)
    {
        CGPathCloseSubpath(xPath);
    }
}

void alignLinePoint(const Point* i_pIn, float& o_fX, float& o_fY)
{
    o_fX = static_cast<float>(i_pIn->getX()) + 0.5;
    o_fY = static_cast<float>(i_pIn->getY()) + 0.5;
}

void getBoundRect(sal_uInt32 nPoints, const Point* pPtAry, tools::Long& rX, tools::Long& rY,
                  tools::Long& rWidth, tools::Long& rHeight)
{
    tools::Long nX1 = pPtAry->getX();
    tools::Long nX2 = nX1;
    tools::Long nY1 = pPtAry->getY();
    tools::Long nY2 = nY1;

    for (sal_uInt32 n = 1; n < nPoints; n++)
    {
        if (pPtAry[n].getX() < nX1)
        {
            nX1 = pPtAry[n].getX();
        }
        else if (pPtAry[n].getX() > nX2)
        {
            nX2 = pPtAry[n].getX();
        }
        if (pPtAry[n].getY() < nY1)
        {
            nY1 = pPtAry[n].getY();
        }
        else if (pPtAry[n].getY() > nY2)
        {
            nY2 = pPtAry[n].getY();
        }
    }
    rX = nX1;
    rY = nY1;
    rWidth = nX2 - nX1 + 1;
    rHeight = nY2 - nY1 + 1;
}

Color ImplGetROPColor(SalROPColor nROPColor)
{
    Color nColor;
    if (nROPColor == SalROPColor::N0)
    {
        nColor = Color(0, 0, 0);
    }
    else
    {
        nColor = Color(255, 255, 255);
    }
    return nColor;
}

void drawPattern50(void*, CGContextRef rContext)
{
    static const CGRect aRects[2] = { { { 0, 0 }, { 2, 2 } }, { { 2, 2 }, { 2, 2 } } };
    CGContextAddRects(rContext, aRects, 2);
    CGContextFillPath(rContext);
}
}

AquaGraphicsBackend::AquaGraphicsBackend(AquaSharedAttributes& rShared)
    : AquaGraphicsBackendBase(rShared, this)
{
}

AquaGraphicsBackend::~AquaGraphicsBackend() {}

void AquaGraphicsBackend::setClipRegion(vcl::Region const& rRegion)
{
    // release old clip path
    mrShared.unsetClipPath();
    mrShared.mxClipPath = CGPathCreateMutable();

    // set current path, either as polypolygon or sequence of rectangles
    RectangleVector aRectangles;
    rRegion.GetRegionRectangles(aRectangles);

    for (const auto& rRect : aRectangles)
    {
        const tools::Long nW(rRect.Right() - rRect.Left() + 1); // uses +1 logic in original

        if (nW)
        {
            const tools::Long nH(rRect.Bottom() - rRect.Top() + 1); // uses +1 logic in original

            if (nH)
            {
                const CGRect aRect = CGRectMake(rRect.Left(), rRect.Top(), nW, nH);
                CGPathAddRect(mrShared.mxClipPath, nullptr, aRect);
            }
        }
    }
    // set the current path as clip region
    if (mrShared.checkContext())
        mrShared.setState();
}

void AquaGraphicsBackend::ResetClipRegion()
{
    // release old path and indicate no clipping
    mrShared.unsetClipPath();

    if (mrShared.checkContext())
    {
        mrShared.setState();
    }
}

sal_uInt16 AquaGraphicsBackend::GetBitCount() const
{
    sal_uInt16 nBits = mrShared.mnBitmapDepth ? mrShared.mnBitmapDepth : 32; //24;
    return nBits;
}

tools::Long AquaGraphicsBackend::GetGraphicsWidth() const
{
    tools::Long width = 0;
    if (mrShared.maContextHolder.isSet()
        && (
#ifndef IOS
               mrShared.mbWindow ||
#endif
               mrShared.mbVirDev))
    {
        width = mrShared.mnWidth;
    }

#ifndef IOS
    if (width == 0)
    {
        if (mrShared.mbWindow && mrShared.mpFrame)
        {
            width = mrShared.mpFrame->GetWidth();
        }
    }
#endif
    return width;
}

void AquaGraphicsBackend::SetLineColor()
{
    mrShared.maLineColor.SetActive(false);
    if (mrShared.checkContext())
    {
        CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), mrShared.maLineColor.GetRed(),
                                   mrShared.maLineColor.GetGreen(), mrShared.maLineColor.GetBlue(),
                                   0.0); // alpha, transparent
    }
}

void AquaGraphicsBackend::SetLineColor(Color nColor)
{
    mrShared.maLineColor = RGBAColor(nColor);
    if (mrShared.checkContext())
    {
        CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), mrShared.maLineColor.GetRed(),
                                   mrShared.maLineColor.GetGreen(), mrShared.maLineColor.GetBlue(),
                                   mrShared.maLineColor.GetAlpha());
    }
}

void AquaGraphicsBackend::SetFillColor()
{
    mrShared.maFillColor.SetActive(false);
    if (mrShared.checkContext())
    {
        CGContextSetRGBFillColor(mrShared.maContextHolder.get(), mrShared.maFillColor.GetRed(),
                                 mrShared.maFillColor.GetGreen(), mrShared.maFillColor.GetBlue(),
                                 0.0); // alpha, transparent
    }
}

void AquaGraphicsBackend::SetFillColor(Color nColor)
{
    mrShared.maFillColor = RGBAColor(nColor);
    if (mrShared.checkContext())
    {
        CGContextSetRGBFillColor(mrShared.maContextHolder.get(), mrShared.maFillColor.GetRed(),
                                 mrShared.maFillColor.GetGreen(), mrShared.maFillColor.GetBlue(),
                                 mrShared.maFillColor.GetAlpha());
    }
}

void AquaGraphicsBackend::SetXORMode(bool bSet, bool bInvertOnly)
{
    // return early if XOR mode remains unchanged
    if (mrShared.mbPrinter)
    {
        return;
    }
    if (!bSet && mrShared.mnXorMode == 2)
    {
        CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeNormal);
        mrShared.mnXorMode = 0;
        return;
    }
    else if (bSet && bInvertOnly && mrShared.mnXorMode == 0)
    {
        CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference);
        mrShared.mnXorMode = 2;
        return;
    }

    if (!mrShared.mpXorEmulation && !bSet)
    {
        return;
    }
    if (mrShared.mpXorEmulation && bSet == mrShared.mpXorEmulation->IsEnabled())
    {
        return;
    }
    if (!mrShared.checkContext())
    {
        return;
    }
    // prepare XOR emulation
    if (!mrShared.mpXorEmulation)
    {
        mrShared.mpXorEmulation = std::make_unique<XorEmulation>();
        mrShared.mpXorEmulation->SetTarget(mrShared.mnWidth, mrShared.mnHeight,
                                           mrShared.mnBitmapDepth, mrShared.maContextHolder.get(),
                                           mrShared.maLayer.get());
    }

    // change the XOR mode
    if (bSet)
    {
        mrShared.mpXorEmulation->Enable();
        mrShared.maContextHolder.set(mrShared.mpXorEmulation->GetMaskContext());
        mrShared.mnXorMode = 1;
    }
    else
    {
        mrShared.mpXorEmulation->UpdateTarget();
        mrShared.mpXorEmulation->Disable();
        mrShared.maContextHolder.set(mrShared.mpXorEmulation->GetTargetContext());
        mrShared.mnXorMode = 0;
    }
}

void AquaGraphicsBackend::SetROPFillColor(SalROPColor nROPColor)
{
    if (!mrShared.mbPrinter)
    {
        SetFillColor(ImplGetROPColor(nROPColor));
    }
}

void AquaGraphicsBackend::SetROPLineColor(SalROPColor nROPColor)
{
    if (!mrShared.mbPrinter)
    {
        SetLineColor(ImplGetROPColor(nROPColor));
    }
}

void AquaGraphicsBackend::drawPixelImpl(tools::Long nX, tools::Long nY, const RGBAColor& rColor)
{
    if (!mrShared.checkContext())
        return;

    // overwrite the fill color
    CGContextSetFillColor(mrShared.maContextHolder.get(), rColor.AsArray());
    // draw 1x1 rect, there is no pixel drawing in Quartz
    const CGRect aDstRect = CGRectMake(nX, nY, 1, 1);
    CGContextFillRect(mrShared.maContextHolder.get(), aDstRect);

    refreshRect(aDstRect);

    // reset the fill color
    CGContextSetFillColor(mrShared.maContextHolder.get(), mrShared.maFillColor.AsArray());
}

void AquaGraphicsBackend::drawPixel(tools::Long nX, tools::Long nY)
{
    // draw pixel with current line color
    drawPixelImpl(nX, nY, mrShared.maLineColor);
}

void AquaGraphicsBackend::drawPixel(tools::Long nX, tools::Long nY, Color nColor)
{
    const RGBAColor aPixelColor(nColor);
    drawPixelImpl(nX, nY, aPixelColor);
}

void AquaGraphicsBackend::drawLine(tools::Long nX1, tools::Long nY1, tools::Long nX2,
                                   tools::Long nY2)
{
    if (nX1 == nX2 && nY1 == nY2)
    {
        // #i109453# platform independent code expects at least one pixel to be drawn
        drawPixel(nX1, nY1);
        return;
    }

    if (!mrShared.checkContext())
        return;

    CGContextBeginPath(mrShared.maContextHolder.get());
    CGContextMoveToPoint(mrShared.maContextHolder.get(), float(nX1) + 0.5, float(nY1) + 0.5);
    CGContextAddLineToPoint(mrShared.maContextHolder.get(), float(nX2) + 0.5, float(nY2) + 0.5);
    CGContextDrawPath(mrShared.maContextHolder.get(), kCGPathStroke);

    tools::Rectangle aRefreshRect(nX1, nY1, nX2, nY2);
    (void)aRefreshRect;
    // Is a call to RefreshRect( aRefreshRect ) missing here?
}

void AquaGraphicsBackend::drawRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
                                   tools::Long nHeight)
{
    if (!mrShared.checkContext())
        return;

    CGRect aRect = CGRectMake(nX, nY, nWidth, nHeight);
    if (mrShared.isPenActive())
    {
        aRect.origin.x += 0.5;
        aRect.origin.y += 0.5;
        aRect.size.width -= 1;
        aRect.size.height -= 1;
    }

    if (mrShared.isBrushActive() && mrShared.maFillColor.GetAlpha() == 0)
    {
        CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeClear);
        CGContextClearRect(mrShared.maContextHolder.get(), aRect);
        CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeNormal);
    }
    else
    {
        if (mrShared.isBrushActive())
        {
            CGContextFillRect(mrShared.maContextHolder.get(), aRect);
        }
        if (mrShared.isPenActive())
        {
            CGContextStrokeRect(mrShared.maContextHolder.get(), aRect);
        }
    }
    mrShared.refreshRect(nX, nY, nWidth, nHeight);
}

void AquaGraphicsBackend::drawPolyLine(sal_uInt32 nPoints, const Point* pPointArray)
{
    if (nPoints < 1)
        return;

    if (!mrShared.checkContext())
        return;

    tools::Long nX = 0, nY = 0, nWidth = 0, nHeight = 0;
    getBoundRect(nPoints, pPointArray, nX, nY, nWidth, nHeight);

    float fX, fY;
    CGContextBeginPath(mrShared.maContextHolder.get());
    alignLinePoint(pPointArray, fX, fY);
    CGContextMoveToPoint(mrShared.maContextHolder.get(), fX, fY);
    pPointArray++;

    for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPointArray++)
    {
        alignLinePoint(pPointArray, fX, fY);
        CGContextAddLineToPoint(mrShared.maContextHolder.get(), fX, fY);
    }
    CGContextStrokePath(mrShared.maContextHolder.get());

    mrShared.refreshRect(nX, nY, nWidth, nHeight);
}

void AquaGraphicsBackend::drawPolygon(sal_uInt32 nPoints, const Point* pPointArray)
{
    if (nPoints <= 1)
        return;

    if (!mrShared.checkContext())
        return;

    tools::Long nX = 0, nY = 0, nWidth = 0, nHeight = 0;
    getBoundRect(nPoints, pPointArray, nX, nY, nWidth, nHeight);

    CGPathDrawingMode eMode;
    if (mrShared.isBrushActive() && mrShared.isPenActive())
    {
        eMode = kCGPathEOFillStroke;
    }
    else if (mrShared.isPenActive())
    {
        eMode = kCGPathStroke;
    }
    else if (mrShared.isBrushActive())
    {
        eMode = kCGPathEOFill;
    }
    else
    {
        SAL_WARN("vcl.quartz", "Neither pen nor brush visible");
        return;
    }

    CGContextBeginPath(mrShared.maContextHolder.get());

    if (mrShared.isPenActive())
    {
        float fX, fY;
        alignLinePoint(pPointArray, fX, fY);
        CGContextMoveToPoint(mrShared.maContextHolder.get(), fX, fY);
        pPointArray++;
        for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPointArray++)
        {
            alignLinePoint(pPointArray, fX, fY);
            CGContextAddLineToPoint(mrShared.maContextHolder.get(), fX, fY);
        }
    }
    else
    {
        CGContextMoveToPoint(mrShared.maContextHolder.get(), pPointArray->getX(),
                             pPointArray->getY());
        pPointArray++;
        for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPointArray++)
        {
            CGContextAddLineToPoint(mrShared.maContextHolder.get(), pPointArray->getX(),
                                    pPointArray->getY());
        }
    }

    CGContextClosePath(mrShared.maContextHolder.get());
    CGContextDrawPath(mrShared.maContextHolder.get(), eMode);

    mrShared.refreshRect(nX, nY, nWidth, nHeight);
}

void AquaGraphicsBackend::drawPolyPolygon(sal_uInt32 nPolyCount, const sal_uInt32* pPoints,
                                          const Point** ppPtAry)
{
    if (nPolyCount <= 0)
        return;

    if (!mrShared.checkContext())
        return;

    // find bound rect
    tools::Long leftX = 0, topY = 0, maxWidth = 0, maxHeight = 0;

    getBoundRect(pPoints[0], ppPtAry[0], leftX, topY, maxWidth, maxHeight);

    for (sal_uInt32 n = 1; n < nPolyCount; n++)
    {
        tools::Long nX = leftX, nY = topY, nW = maxWidth, nH = maxHeight;
        getBoundRect(pPoints[n], ppPtAry[n], nX, nY, nW, nH);
        if (nX < leftX)
        {
            maxWidth += leftX - nX;
            leftX = nX;
        }
        if (nY < topY)
        {
            maxHeight += topY - nY;
            topY = nY;
        }
        if (nX + nW > leftX + maxWidth)
        {
            maxWidth = nX + nW - leftX;
        }
        if (nY + nH > topY + maxHeight)
        {
            maxHeight = nY + nH - topY;
        }
    }

    // prepare drawing mode
    CGPathDrawingMode eMode;
    if (mrShared.isBrushActive() && mrShared.isPenActive())
    {
        eMode = kCGPathEOFillStroke;
    }
    else if (mrShared.isPenActive())
    {
        eMode = kCGPathStroke;
    }
    else if (mrShared.isBrushActive())
    {
        eMode = kCGPathEOFill;
    }
    else
    {
        SAL_WARN("vcl.quartz", "Neither pen nor brush visible");
        return;
    }

    // convert to CGPath
    CGContextBeginPath(mrShared.maContextHolder.get());
    if (mrShared.isPenActive())
    {
        for (sal_uInt32 nPoly = 0; nPoly < nPolyCount; nPoly++)
        {
            const sal_uInt32 nPoints = pPoints[nPoly];
            if (nPoints > 1)
            {
                const Point* pPtAry = ppPtAry[nPoly];
                float fX, fY;

                alignLinePoint(pPtAry, fX, fY);
                CGContextMoveToPoint(mrShared.maContextHolder.get(), fX, fY);
                pPtAry++;

                for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++)
                {
                    alignLinePoint(pPtAry, fX, fY);
                    CGContextAddLineToPoint(mrShared.maContextHolder.get(), fX, fY);
                }
                CGContextClosePath(mrShared.maContextHolder.get());
            }
        }
    }
    else
    {
        for (sal_uInt32 nPoly = 0; nPoly < nPolyCount; nPoly++)
        {
            const sal_uInt32 nPoints = pPoints[nPoly];
            if (nPoints > 1)
            {
                const Point* pPtAry = ppPtAry[nPoly];
                CGContextMoveToPoint(mrShared.maContextHolder.get(), pPtAry->getX(),
                                     pPtAry->getY());
                pPtAry++;
                for (sal_uInt32 nPoint = 1; nPoint < nPoints; nPoint++, pPtAry++)
                {
                    CGContextAddLineToPoint(mrShared.maContextHolder.get(), pPtAry->getX(),
                                            pPtAry->getY());
                }
                CGContextClosePath(mrShared.maContextHolder.get());
            }
        }
    }

    CGContextDrawPath(mrShared.maContextHolder.get(), eMode);

    mrShared.refreshRect(leftX, topY, maxWidth, maxHeight);
}

void AquaGraphicsBackend::drawPolyPolygon(const basegfx::B2DHomMatrix& rObjectToDevice,
                                          const basegfx::B2DPolyPolygon& rPolyPolygon,
                                          double fTransparency)
{
#ifdef IOS
    if (!mrShared.maContextHolder.isSet())
        return;
#endif

    // short circuit if there is nothing to do
    if (rPolyPolygon.count() == 0)
        return;

    // ignore invisible polygons
    if ((fTransparency >= 1.0) || (fTransparency < 0))
        return;

    // Fallback: Transform to DeviceCoordinates
    basegfx::B2DPolyPolygon aPolyPolygon(rPolyPolygon);
    aPolyPolygon.transform(rObjectToDevice);

    // setup poly-polygon path
    CGMutablePathRef xPath = CGPathCreateMutable();
    // tdf#120252 Use the correct, already transformed PolyPolygon (as long as
    // the transformation is not used here...)
    for (auto const& rPolygon : std::as_const(aPolyPolygon))
    {
        AddPolygonToPath(xPath, rPolygon, true, !getAntiAlias(), mrShared.isPenActive());
    }

    const CGRect aRefreshRect = CGPathGetBoundingBox(xPath);
    // #i97317# workaround for Quartz having problems with drawing small polygons
    if (aRefreshRect.size.width > 0.125 || aRefreshRect.size.height > 0.125)
    {
        // prepare drawing mode
        CGPathDrawingMode eMode;
        if (mrShared.isBrushActive() && mrShared.isPenActive())
        {
            eMode = kCGPathEOFillStroke;
        }
        else if (mrShared.isPenActive())
        {
            eMode = kCGPathStroke;
        }
        else if (mrShared.isBrushActive())
        {
            eMode = kCGPathEOFill;
        }
        else
        {
            SAL_WARN("vcl.quartz", "Neither pen nor brush visible");
            CGPathRelease(xPath);
            return;
        }

        // use the path to prepare the graphics context
        mrShared.maContextHolder.saveState();
        CGContextBeginPath(mrShared.maContextHolder.get());
        CGContextAddPath(mrShared.maContextHolder.get(), xPath);

        // draw path with antialiased polygon
        CGContextSetShouldAntialias(mrShared.maContextHolder.get(), getAntiAlias());
        CGContextSetAlpha(mrShared.maContextHolder.get(), 1.0 - fTransparency);
        CGContextDrawPath(mrShared.maContextHolder.get(), eMode);
        mrShared.maContextHolder.restoreState();

        // mark modified rectangle as updated
        refreshRect(aRefreshRect);
    }

    CGPathRelease(xPath);
}

bool AquaGraphicsBackend::drawPolyLine(const basegfx::B2DHomMatrix& rObjectToDevice,
                                       const basegfx::B2DPolygon& rPolyLine, double fTransparency,
                                       double fLineWidth,
                                       const std::vector<double>* pStroke, // MM01
                                       basegfx::B2DLineJoin eLineJoin,
                                       css::drawing::LineCap eLineCap, double fMiterMinimumAngle,
                                       bool bPixelSnapHairline)
{
    // MM01 check done for simple reasons
    if (!rPolyLine.count() || fTransparency < 0.0 || fTransparency > 1.0)
    {
        return true;
    }

#ifdef IOS
    if (!mrShared.checkContext())
        return false;
#endif

    // tdf#124848 get correct LineWidth in discrete coordinates,
    if (fLineWidth == 0) // hairline
        fLineWidth = 1.0;
    else // Adjust line width for object-to-device scale.
        fLineWidth = (rObjectToDevice * basegfx::B2DVector(fLineWidth, 0)).getLength();

    // #i101491# Aqua does not support B2DLineJoin::NONE; return false to use
    // the fallback (own geometry preparation)
    // #i104886# linejoin-mode and thus the above only applies to "fat" lines
    if ((basegfx::B2DLineJoin::NONE == eLineJoin) && (fLineWidth > 1.3))
        return false;

    // MM01 need to do line dashing as fallback stuff here now
    const double fDotDashLength(
        nullptr != pStroke ? std::accumulate(pStroke->begin(), pStroke->end(), 0.0) : 0.0);
    const bool bStrokeUsed(0.0 != fDotDashLength);
    assert(!bStrokeUsed || (bStrokeUsed && pStroke));
    basegfx::B2DPolyPolygon aPolyPolygonLine;

    if (bStrokeUsed)
    {
        // apply LineStyle
        basegfx::utils::applyLineDashing(rPolyLine, // source
                                         *pStroke, // pattern
                                         &aPolyPolygonLine, // target for lines
                                         nullptr, // target for gaps
                                         fDotDashLength); // full length if available
    }
    else
    {
        // no line dashing, just copy
        aPolyPolygonLine.append(rPolyLine);
    }

    // Transform to DeviceCoordinates, get DeviceLineWidth, execute PixelSnapHairline
    aPolyPolygonLine.transform(rObjectToDevice);
    if (bPixelSnapHairline)
    {
        aPolyPolygonLine = basegfx::utils::snapPointsOfHorizontalOrVerticalEdges(aPolyPolygonLine);
    }

    // setup line attributes
    CGLineJoin aCGLineJoin = kCGLineJoinMiter;
    switch (eLineJoin)
    {
        case basegfx::B2DLineJoin::NONE:
            aCGLineJoin = /*TODO?*/ kCGLineJoinMiter;
            break;
        case basegfx::B2DLineJoin::Bevel:
            aCGLineJoin = kCGLineJoinBevel;
            break;
        case basegfx::B2DLineJoin::Miter:
            aCGLineJoin = kCGLineJoinMiter;
            break;
        case basegfx::B2DLineJoin::Round:
            aCGLineJoin = kCGLineJoinRound;
            break;
    }
    // convert miter minimum angle to miter limit
    CGFloat fCGMiterLimit = 1.0 / sin(std::max(fMiterMinimumAngle, 0.01 * M_PI) / 2.0);
    // setup cap attribute
    CGLineCap aCGLineCap(kCGLineCapButt);

    switch (eLineCap)
    {
        default: // css::drawing::LineCap_BUTT:
        {
            aCGLineCap = kCGLineCapButt;
            break;
        }
        case css::drawing::LineCap_ROUND:
        {
            aCGLineCap = kCGLineCapRound;
            break;
        }
        case css::drawing::LineCap_SQUARE:
        {
            aCGLineCap = kCGLineCapSquare;
            break;
        }
    }

    // setup poly-polygon path
    CGMutablePathRef xPath = CGPathCreateMutable();

    // MM01 todo - I assume that this is OKAY to be done in one run for quartz
    // but this NEEDS to be checked/verified
    for (sal_uInt32 a(0); a < aPolyPolygonLine.count(); a++)
    {
        const basegfx::B2DPolygon aPolyLine(aPolyPolygonLine.getB2DPolygon(a));
        AddPolygonToPath(xPath, aPolyLine, aPolyLine.isClosed(), !getAntiAlias(), true);
    }

    const CGRect aRefreshRect = CGPathGetBoundingBox(xPath);
    // #i97317# workaround for Quartz having problems with drawing small polygons
    if ((aRefreshRect.size.width > 0.125) || (aRefreshRect.size.height > 0.125))
    {
        // use the path to prepare the graphics context
        mrShared.maContextHolder.saveState();
        CGContextBeginPath(mrShared.maContextHolder.get());
        CGContextAddPath(mrShared.maContextHolder.get(), xPath);
        // draw path with antialiased line
        CGContextSetShouldAntialias(mrShared.maContextHolder.get(), getAntiAlias());
        CGContextSetAlpha(mrShared.maContextHolder.get(), 1.0 - fTransparency);
        CGContextSetLineJoin(mrShared.maContextHolder.get(), aCGLineJoin);
        CGContextSetLineCap(mrShared.maContextHolder.get(), aCGLineCap);
        CGContextSetLineWidth(mrShared.maContextHolder.get(), fLineWidth);
        CGContextSetMiterLimit(mrShared.maContextHolder.get(), fCGMiterLimit);
        CGContextDrawPath(mrShared.maContextHolder.get(), kCGPathStroke);
        mrShared.maContextHolder.restoreState();

        // mark modified rectangle as updated
        refreshRect(aRefreshRect);
    }

    CGPathRelease(xPath);

    return true;
}

bool AquaGraphicsBackend::drawPolyLineBezier(sal_uInt32 /*nPoints*/, const Point* /*pPointArray*/,
                                             const PolyFlags* /*pFlagArray*/)
{
    return false;
}

bool AquaGraphicsBackend::drawPolygonBezier(sal_uInt32 /*nPoints*/, const Point* /*pPointArray*/,
                                            const PolyFlags* /*pFlagArray*/)
{
    return false;
}

bool AquaGraphicsBackend::drawPolyPolygonBezier(sal_uInt32 /*nPoly*/, const sal_uInt32* /*pPoints*/,
                                                const Point* const* /*pPointArray*/,
                                                const PolyFlags* const* /*pFlagArray*/)
{
    return false;
}

void AquaGraphicsBackend::drawBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap)
{
    if (!mrShared.checkContext())
        return;

    CGImageRef xImage = rSalBitmap.CreateCroppedImage(
        static_cast<int>(rPosAry.mnSrcX), static_cast<int>(rPosAry.mnSrcY),
        static_cast<int>(rPosAry.mnSrcWidth), static_cast<int>(rPosAry.mnSrcHeight));
    if (!xImage)
        return;

    const CGRect aDstRect
        = CGRectMake(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight);
    CGContextDrawImage(mrShared.maContextHolder.get(), aDstRect, xImage);

    CGImageRelease(xImage);
    refreshRect(aDstRect);
}

void AquaGraphicsBackend::drawMask(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap,
                                   Color nMaskColor)
{
    if (!mrShared.checkContext())
        return;

    CGImageRef xImage = rSalBitmap.CreateColorMask(
        rPosAry.mnSrcX, rPosAry.mnSrcY, rPosAry.mnSrcWidth, rPosAry.mnSrcHeight, nMaskColor);
    if (!xImage)
        return;

    const CGRect aDstRect
        = CGRectMake(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight);
    CGContextDrawImage(mrShared.maContextHolder.get(), aDstRect, xImage);
    CGImageRelease(xImage);
    refreshRect(aDstRect);
}

std::shared_ptr<SalBitmap> AquaGraphicsBackend::getBitmap(tools::Long nX, tools::Long nY,
                                                          tools::Long nDX, tools::Long nDY,
                                                          bool /*bWithoutAlpha*/)
{
    SAL_WARN_IF(!mrShared.maLayer.isSet(), "vcl.quartz",
                "AquaSalGraphics::getBitmap() with no layer this=" << this);

    mrShared.applyXorContext();

    std::shared_ptr<QuartzSalBitmap> pBitmap = std::make_shared<QuartzSalBitmap>();
    if (!pBitmap->Create(mrShared.maLayer, mrShared.mnBitmapDepth, nX, nY, nDX, nDY,
                         mrShared.isFlipped()))
    {
        pBitmap = nullptr;
    }
    return pBitmap;
}

Color AquaGraphicsBackend::getPixel(tools::Long nX, tools::Long nY)
{
    // return default value on printers or when out of bounds
    if (!mrShared.maLayer.isSet() || (nX < 0) || (nX >= mrShared.mnWidth) || (nY < 0)
        || (nY >= mrShared.mnHeight))
    {
        return COL_BLACK;
    }

        // prepare creation of matching a CGBitmapContext
#if defined OSL_BIGENDIAN
    struct
    {
        unsigned char b, g, r, a;
    } aPixel;
#else
    struct
    {
        unsigned char a, r, g, b;
    } aPixel;
#endif

    // create a one-pixel bitmap context
    // TODO: is it worth to cache it?
    CGContextRef xOnePixelContext = CGBitmapContextCreate(
        &aPixel, 1, 1, 8, 32, GetSalData()->mxRGBSpace,
        uint32_t(kCGImageAlphaNoneSkipFirst) | uint32_t(kCGBitmapByteOrder32Big));

    // update this graphics layer
    mrShared.applyXorContext();

    // copy the requested pixel into the bitmap context
    if (mrShared.isFlipped())
    {
        nY = mrShared.mnHeight - nY;
    }
    const CGPoint aCGPoint = CGPointMake(-nX, -nY);
    CGContextDrawLayerAtPoint(xOnePixelContext, aCGPoint, mrShared.maLayer.get());

    CGContextRelease(xOnePixelContext);

    Color nColor(ColorAlpha, aPixel.a, aPixel.r, aPixel.g, aPixel.b);
    return nColor;
}

void AquaSalGraphics::GetResolution(sal_Int32& rDPIX, sal_Int32& rDPIY)
{
#ifndef IOS
    if (!mnRealDPIY)
    {
        initResolution((maShared.mbWindow && maShared.mpFrame) ? maShared.mpFrame->getNSWindow()
                                                               : nil);
    }

    if (maShared.mbPrinter)
    {
        rDPIX = rDPIY = 720;
    }
    else
    {
        rDPIX = mnRealDPIX;
        rDPIY = mnRealDPIY;
    }
#else
    // This *must* be 96 or else the iOS app will behave very badly (tiles are scaled wrongly and
    // don't match each others at their boundaries, and other issues). But *why* it must be 96 I
    // have no idea. The commit that changed it to 96 from (the arbitrary) 200 did not say. If you
    // know where else 96 is explicitly or implicitly hard-coded, please modify this comment.

    // Follow-up: It might be this: in 'online', browser/src/map/Map.js:
    // 15 = 1440 twips-per-inch / 96 dpi.
    // Chosen to match previous hardcoded value of 3840 for
    // the current tile pixel size of 256.
    rDPIX = rDPIY = 96;
#endif
}

void AquaGraphicsBackend::pattern50Fill()
{
    static const CGFloat aFillCol[4] = { 1, 1, 1, 1 };
    static const CGPatternCallbacks aCallback = { 0, &drawPattern50, nullptr };
    static const CGColorSpaceRef mxP50Space = CGColorSpaceCreatePattern(GetSalData()->mxRGBSpace);
    static const CGPatternRef mxP50Pattern
        = CGPatternCreate(nullptr, CGRectMake(0, 0, 4, 4), CGAffineTransformIdentity, 4, 4,
                          kCGPatternTilingConstantSpacing, false, &aCallback);
    SAL_WARN_IF(!mrShared.maContextHolder.get(), "vcl.quartz", "maContextHolder.get() is NULL");
    CGContextSetFillColorSpace(mrShared.maContextHolder.get(), mxP50Space);
    CGContextSetFillPattern(mrShared.maContextHolder.get(), mxP50Pattern, aFillCol);
    CGContextFillPath(mrShared.maContextHolder.get());
}

void AquaGraphicsBackend::invert(tools::Long nX, tools::Long nY, tools::Long nWidth,
                                 tools::Long nHeight, SalInvert nFlags)
{
    if (mrShared.checkContext())
    {
        CGRect aCGRect = CGRectMake(nX, nY, nWidth, nHeight);
        mrShared.maContextHolder.saveState();
        if (nFlags & SalInvert::TrackFrame)
        {
            const CGFloat dashLengths[2] = { 4.0, 4.0 }; // for drawing dashed line
            CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference);
            CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0);
            CGContextSetLineDash(mrShared.maContextHolder.get(), 0, dashLengths, 2);
            CGContextSetLineWidth(mrShared.maContextHolder.get(), 2.0);
            CGContextStrokeRect(mrShared.maContextHolder.get(), aCGRect);
        }
        else if (nFlags & SalInvert::N50)
        {
            //CGContextSetAllowsAntialiasing( maContextHolder.get(), false );
            CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference);
            CGContextAddRect(mrShared.maContextHolder.get(), aCGRect);
            pattern50Fill();
        }
        else // just invert
        {
            CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference);
            CGContextSetRGBFillColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0);
            CGContextFillRect(mrShared.maContextHolder.get(), aCGRect);
        }
        mrShared.maContextHolder.restoreState();
        refreshRect(aCGRect);
    }
}

namespace
{
CGPoint* makeCGptArray(sal_uInt32 nPoints, const Point* pPtAry)
{
    CGPoint* CGpoints = new CGPoint[nPoints];
    for (sal_uLong i = 0; i < nPoints; i++)
    {
        CGpoints[i].x = pPtAry[i].getX();
        CGpoints[i].y = pPtAry[i].getY();
    }
    return CGpoints;
}

} // end anonymous ns

void AquaGraphicsBackend::invert(sal_uInt32 nPoints, const Point* pPtAry, SalInvert nSalFlags)
{
    if (mrShared.checkContext())
    {
        mrShared.maContextHolder.saveState();
        CGPoint* CGpoints = makeCGptArray(nPoints, pPtAry);
        CGContextAddLines(mrShared.maContextHolder.get(), CGpoints, nPoints);
        if (nSalFlags & SalInvert::TrackFrame)
        {
            const CGFloat dashLengths[2] = { 4.0, 4.0 }; // for drawing dashed line
            CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference);
            CGContextSetRGBStrokeColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0);
            CGContextSetLineDash(mrShared.maContextHolder.get(), 0, dashLengths, 2);
            CGContextSetLineWidth(mrShared.maContextHolder.get(), 2.0);
            CGContextStrokePath(mrShared.maContextHolder.get());
        }
        else if (nSalFlags & SalInvert::N50)
        {
            CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference);
            pattern50Fill();
        }
        else // just invert
        {
            CGContextSetBlendMode(mrShared.maContextHolder.get(), kCGBlendModeDifference);
            CGContextSetRGBFillColor(mrShared.maContextHolder.get(), 1.0, 1.0, 1.0, 1.0);
            CGContextFillPath(mrShared.maContextHolder.get());
        }
        const CGRect aRefreshRect = CGContextGetClipBoundingBox(mrShared.maContextHolder.get());
        mrShared.maContextHolder.restoreState();
        delete[] CGpoints;
        refreshRect(aRefreshRect);
    }
}

#ifndef IOS
bool AquaGraphicsBackend::drawEPS(tools::Long nX, tools::Long nY, tools::Long nWidth,
                                  tools::Long nHeight, void* pEpsData, sal_uInt32 nByteCount)
{
    // convert the raw data to an NSImageRef
    NSData* xNSData = [NSData dataWithBytes:pEpsData length:static_cast<int>(nByteCount)];
    SAL_WNODEPRECATED_DECLARATIONS_PUSH
    // 'NSEPSImageRep' is deprecated: first deprecated in macOS 14.0 - `NSEPSImageRep` instances
    // cannot be created on macOS 14.0 and later
    NSImageRep* xEpsImage = [NSEPSImageRep imageRepWithData:xNSData];
    SAL_WNODEPRECATED_DECLARATIONS_POP
    if (!xEpsImage)
    {
        return false;
    }
    // get the target context
    if (!mrShared.checkContext())
    {
        return false;
    }
    // NOTE: flip drawing, else the nsimage would be drawn upside down
    mrShared.maContextHolder.saveState();
    //  CGContextTranslateCTM( maContextHolder.get(), 0, +mnHeight );
    CGContextScaleCTM(mrShared.maContextHolder.get(), +1, -1);
    nY = /*mnHeight*/ -(nY + nHeight);

    // prepare the target context
    NSGraphicsContext* pOrigNSCtx = [NSGraphicsContext currentContext];
    [pOrigNSCtx retain];

    // create new context
    NSGraphicsContext* pDrawNSCtx =
        [NSGraphicsContext graphicsContextWithCGContext:mrShared.maContextHolder.get()
                                                flipped:mrShared.isFlipped()];
    // set it, setCurrentContext also releases the previously set one
    [NSGraphicsContext setCurrentContext:pDrawNSCtx];

    // draw the EPS
    const NSRect aDstRect = NSMakeRect(nX, nY, nWidth, nHeight);
    const bool bOK = [xEpsImage drawInRect:aDstRect];

    // restore the NSGraphicsContext
    [NSGraphicsContext setCurrentContext:pOrigNSCtx];
    [pOrigNSCtx release]; // restore the original retain count

    mrShared.maContextHolder.restoreState();
    // mark the destination rectangle as updated
    refreshRect(aDstRect);

    return bOK;
}
#else
bool AquaGraphicsBackend::drawEPS(tools::Long /*nX*/, tools::Long /*nY*/, tools::Long /*nWidth*/,
                                  tools::Long /*nHeight*/, void* /*pEpsData*/,
                                  sal_uInt32 /*nByteCount*/)
{
    return false;
}
#endif

void AquaGraphicsBackend::drawAlphaBitmap(const SalTwoRect& rPosAry, const SalBitmap& rSalBitmap)
{
    if (!mrShared.checkContext())
    {
        assert(false);
        return;
    }

    CGImageRef xImage = rSalBitmap.CreateCroppedImage(
        static_cast<int>(rPosAry.mnSrcX), static_cast<int>(rPosAry.mnSrcY),
        static_cast<int>(rPosAry.mnSrcWidth), static_cast<int>(rPosAry.mnSrcHeight));
    assert(xImage);
    if (!xImage)
        return;

    const CGRect aDstRect
        = CGRectMake(rPosAry.mnDestX, rPosAry.mnDestY, rPosAry.mnDestWidth, rPosAry.mnDestHeight);
    CGContextDrawImage(mrShared.maContextHolder.get(), aDstRect, xImage);

    CGImageRelease(xImage);
    refreshRect(aDstRect);
}

bool AquaGraphicsBackend::drawTransformedBitmap(const basegfx::B2DPoint& rNull,
                                                const basegfx::B2DPoint& rX,
                                                const basegfx::B2DPoint& rY,
                                                const SalBitmap& rSrcBitmap, double fAlpha)
{
    if (!mrShared.checkContext())
        return true;

    if (fAlpha != 1.0)
        return false;

    // get the Quartz image
    const Size aSize = rSrcBitmap.GetSize();
    CGImageRef xImage
        = rSrcBitmap.CreateCroppedImage(0, 0, int(aSize.Width()), int(aSize.Height()));

    if (!xImage)
        return false;

    // setup the image transformation
    // using the rNull,rX,rY points as destinations for the (0,0),(0,Width),(Height,0) source points
    mrShared.maContextHolder.saveState();
    const basegfx::B2DVector aXRel = rX - rNull;
    const basegfx::B2DVector aYRel = rY - rNull;
    const CGAffineTransform aCGMat = CGAffineTransformMake(
        aXRel.getX() / aSize.Width(), aXRel.getY() / aSize.Width(), aYRel.getX() / aSize.Height(),
        aYRel.getY() / aSize.Height(), rNull.getX(), rNull.getY());

    CGContextConcatCTM(mrShared.maContextHolder.get(), aCGMat);

    // draw the transformed image
    const CGRect aSrcRect = CGRectMake(0, 0, aSize.Width(), aSize.Height());
    CGContextDrawImage(mrShared.maContextHolder.get(), aSrcRect, xImage);

    CGImageRelease(xImage);

    // restore the Quartz graphics state
    mrShared.maContextHolder.restoreState();

    // mark the destination as painted
    const CGRect aDstRect = CGRectApplyAffineTransform(aSrcRect, aCGMat);
    refreshRect(aDstRect);

    return true;
}

bool AquaGraphicsBackend::drawAlphaRect(tools::Long nX, tools::Long nY, tools::Long nWidth,
                                        tools::Long nHeight, sal_uInt8 nTransparency)
{
    if (!mrShared.checkContext())
        return true;

    // save the current state
    mrShared.maContextHolder.saveState();
    CGContextSetAlpha(mrShared.maContextHolder.get(), (100 - nTransparency) * (1.0 / 100));

    CGRect aRect = CGRectMake(nX, nY, nWidth - 1, nHeight - 1);
    if (mrShared.isPenActive())
    {
        aRect.origin.x += 0.5;
        aRect.origin.y += 0.5;
    }

    CGContextBeginPath(mrShared.maContextHolder.get());
    CGContextAddRect(mrShared.maContextHolder.get(), aRect);
    CGContextDrawPath(mrShared.maContextHolder.get(), kCGPathFill);

    mrShared.maContextHolder.restoreState();
    refreshRect(aRect);

    return true;
}

bool AquaGraphicsBackend::drawGradient(const tools::PolyPolygon& /*rPolygon*/,
                                       const Gradient& /*rGradient*/)
{
    return false;
}

bool AquaGraphicsBackend::implDrawGradient(basegfx::B2DPolyPolygon const& /*rPolyPolygon*/,
                                           SalGradient const& /*rGradient*/)
{
    return false;
}

bool AquaGraphicsBackend::supportsOperation(OutDevSupportType /*eType*/) const { return false; }

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