/*
 * Copyright (C) 2020 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "DisplayBoxDecorationPainter.h"

#if ENABLE(LAYOUT_FORMATTING_CONTEXT)

#include "BorderEdge.h" // For BoxSideSet.
#include "Color.h"
#include "DisplayBoxDecorationData.h"
#include "DisplayBoxModelBox.h"
#include "DisplayBoxRareGeometry.h"
#include "DisplayPaintingContext.h"
#include "DisplayStyle.h"
#include "DisplayTree.h"
#include "FillLayer.h"
#include "GeometryUtilities.h"
#include "GraphicsContext.h"
#include "IntRect.h"
#include "LayoutPoint.h"
#include "ShadowData.h"

namespace WebCore {
namespace Display {

class BorderPainter {
public:
    BorderPainter(const RectEdges<BorderEdge>& edges, const FloatRoundedRect& borderRect, BackgroundBleedAvoidance bleedAvoidance, bool includeLeftEdge, bool includeRightEdge)
        : m_edges(edges)
        , m_borderRect(borderRect)
        , m_bleedAvoidance(bleedAvoidance)
        , m_includeLeftEdge(includeLeftEdge)
        , m_includeRightEdge(includeRightEdge)
    {
    }
    
    void paintBorders(PaintingContext&) const;

private:
    static bool edgesShareColor(const BorderEdge& firstEdge, const BorderEdge& secondEdge)
    {
        return firstEdge.color() == secondEdge.color();
    }

    static bool borderStyleFillsBorderArea(BorderStyle style)
    {
        return !(style == BorderStyle::Dotted || style == BorderStyle::Dashed || style == BorderStyle::Double);
    }

    static bool borderStyleHasInnerDetail(BorderStyle style)
    {
        return style == BorderStyle::Groove || style == BorderStyle::Ridge || style == BorderStyle::Double;
    }

    static bool styleRequiresClipPolygon(BorderStyle style)
    {
        return style == BorderStyle::Dotted || style == BorderStyle::Dashed; // These are drawn with a stroke, so we have to clip to get corner miters.
    }

    static bool borderStyleIsDottedOrDashed(BorderStyle style)
    {
        return style == BorderStyle::Dotted || style == BorderStyle::Dashed;
    }

    static bool borderWillArcInnerEdge(const FloatSize& firstRadius, const FloatSize& secondRadius)
    {
        return !firstRadius.isZero() || !secondRadius.isZero();
    }

    static bool borderStyleHasUnmatchedColorsAtCorner(BorderStyle, BoxSide, BoxSide adjacentSide);
    static bool borderStylesRequireMitre(BoxSide, BoxSide adjacentSide, BorderStyle, BorderStyle adjacentStyle);
    static FloatRoundedRect calculateAdjustedInnerBorder(const FloatRoundedRect& innerBorder, BoxSide);
    static void calculateBorderStyleColor(BorderStyle, BoxSide, Color&);

    FloatRect borderInnerRectAdjustedForBleedAvoidance(const PaintingContext&) const;

    bool colorsMatchAtCorner(BoxSide, BoxSide adjacentSide) const;
    bool colorNeedsAntiAliasAtCorner(BoxSide, BoxSide adjacentSide) const;
    bool willBeOverdrawn(BoxSide, BoxSide adjacentSide) const;
    bool joinRequiresMitre(BoxSide, BoxSide adjacentSide, bool allowOverdraw) const;

    void clipBorderSidePolygon(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, BoxSide, bool firstEdgeMatches, bool secondEdgeMatches) const;

    void drawLineForBoxSide(PaintingContext&, const FloatRect&, BoxSide, Color, BorderStyle, float adjacentWidth1, float adjacentWidth2, bool antialias) const;
    void drawBoxSideFromPath(PaintingContext&, const FloatRect& borderRect, const Path& borderPath, float thickness, float drawThickness, BoxSide, Color, BorderStyle) const;

    void paintOneBorderSide(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, const FloatRect& sideRect, BoxSide, const Path*, bool antialias, const Color* overrideColor) const;
    void paintBorderSides(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet, bool antialias, const Color* overrideColor = nullptr) const;
    void paintTranslucentBorderSides(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet, bool antialias) const;

    const RectEdges<BorderEdge>& m_edges;
    const FloatRoundedRect m_borderRect;
    const BackgroundBleedAvoidance m_bleedAvoidance;
    const bool m_includeLeftEdge;
    const bool m_includeRightEdge;
};

// BorderStyle::Outset darkens the bottom and right (and maybe lightens the top and left)
// BorderStyle::Inset darkens the top and left (and maybe lightens the bottom and right)
bool BorderPainter::borderStyleHasUnmatchedColorsAtCorner(BorderStyle style, BoxSide side, BoxSide adjacentSide)
{
    // These styles match at the top/left and bottom/right.
    if (style == BorderStyle::Inset || style == BorderStyle::Groove || style == BorderStyle::Ridge || style == BorderStyle::Outset) {
        BoxSideSet topRightSides = { BoxSideFlag::Top, BoxSideFlag::Right };
        BoxSideSet bottomLeftSides = { BoxSideFlag::Bottom, BoxSideFlag::Left };

        BoxSideSet usedSides { edgeFlagForSide(side), edgeFlagForSide(adjacentSide) };
        return usedSides == topRightSides || usedSides == bottomLeftSides;
    }
    return false;
}

bool BorderPainter::colorsMatchAtCorner(BoxSide side, BoxSide adjacentSide) const
{
    auto& edge = m_edges.at(side);
    auto& adjacentEdge = m_edges.at(adjacentSide);

    if (edge.shouldRender() != adjacentEdge.shouldRender())
        return false;

    if (!edgesShareColor(edge, adjacentEdge))
        return false;

    return !borderStyleHasUnmatchedColorsAtCorner(edge.style(), side, adjacentSide);
}

bool BorderPainter::colorNeedsAntiAliasAtCorner(BoxSide side, BoxSide adjacentSide) const
{
    auto& edge = m_edges.at(side);
    auto& adjacentEdge = m_edges.at(adjacentSide);

    if (edge.color().isOpaque())
        return false;

    if (edge.shouldRender() != adjacentEdge.shouldRender())
        return false;

    if (!edgesShareColor(edge, adjacentEdge))
        return true;

    return borderStyleHasUnmatchedColorsAtCorner(edge.style(), side, adjacentSide);
}

void BorderPainter::calculateBorderStyleColor(BorderStyle style, BoxSide side, Color& color)
{
    ASSERT(style == BorderStyle::Inset || style == BorderStyle::Outset);

    // This values were derived empirically.
    constexpr float baseDarkColorLuminance { 0.014443844f }; // Luminance of SRGBA<uint8_t> { 32, 32, 32 }
    constexpr float baseLightColorLuminance { 0.83077f }; // Luminance of SRGBA<uint8_t> { 235, 235, 235 }

    enum Operation { Darken, Lighten };

    Operation operation = (side == BoxSide::Top || side == BoxSide::Left) == (style == BorderStyle::Inset) ? Darken : Lighten;

    // Here we will darken the border decoration color when needed. This will yield a similar behavior as in FF.
    if (operation == Darken) {
        if (color.luminance() > baseDarkColorLuminance)
            color = color.darkened();
    } else {
        if (color.luminance() < baseLightColorLuminance)
            color = color.lightened();
    }
}

// This assumes that we draw in order: top, bottom, left, right.
bool BorderPainter::willBeOverdrawn(BoxSide side, BoxSide adjacentSide) const
{
    switch (side) {
    case BoxSide::Top:
    case BoxSide::Bottom: {
        auto& edge = m_edges.at(side);
        auto& adjacentEdge = m_edges.at(adjacentSide);

        if (adjacentEdge.presentButInvisible())
            return false;

        if (!edgesShareColor(edge, adjacentEdge) && !adjacentEdge.color().isOpaque())
            return false;
        
        if (!borderStyleFillsBorderArea(adjacentEdge.style()))
            return false;

        return true;
    }
    case BoxSide::Left:
    case BoxSide::Right:
        // These draw last, so are never overdrawn.
        return false;
    }
    return false;
}

bool BorderPainter::borderStylesRequireMitre(BoxSide side, BoxSide adjacentSide, BorderStyle style, BorderStyle adjacentStyle)
{
    if (style == BorderStyle::Double || adjacentStyle == BorderStyle::Double || adjacentStyle == BorderStyle::Groove || adjacentStyle == BorderStyle::Ridge)
        return true;

    if (borderStyleIsDottedOrDashed(style) != borderStyleIsDottedOrDashed(adjacentStyle))
        return true;

    if (style != adjacentStyle)
        return true;

    return borderStyleHasUnmatchedColorsAtCorner(style, side, adjacentSide);
}

FloatRoundedRect BorderPainter::calculateAdjustedInnerBorder(const FloatRoundedRect& innerBorder, BoxSide side)
{
    // Expand the inner border as necessary to make it a rounded rect (i.e. radii contained within each edge).
    // This function relies on the fact we only get radii not contained within each edge if one of the radii
    // for an edge is zero, so we can shift the arc towards the zero radius corner.
    auto newRadii = innerBorder.radii();
    auto newRect = innerBorder.rect();

    float overshoot;
    float maxRadii;

    switch (side) {
    case BoxSide::Top:
        overshoot = newRadii.topLeft().width() + newRadii.topRight().width() - newRect.width();
        if (overshoot > 0) {
            ASSERT(!(newRadii.topLeft().width() && newRadii.topRight().width()));
            newRect.setWidth(newRect.width() + overshoot);
            if (!newRadii.topLeft().width())
                newRect.move(-overshoot, 0);
        }
        newRadii.setBottomLeft({ });
        newRadii.setBottomRight({ });
        maxRadii = std::max(newRadii.topLeft().height(), newRadii.topRight().height());
        if (maxRadii > newRect.height())
            newRect.setHeight(maxRadii);
        break;

    case BoxSide::Bottom:
        overshoot = newRadii.bottomLeft().width() + newRadii.bottomRight().width() - newRect.width();
        if (overshoot > 0) {
            ASSERT(!(newRadii.bottomLeft().width() && newRadii.bottomRight().width()));
            newRect.setWidth(newRect.width() + overshoot);
            if (!newRadii.bottomLeft().width())
                newRect.move(-overshoot, 0);
        }
        newRadii.setTopLeft({ });
        newRadii.setTopRight({ });
        maxRadii = std::max(newRadii.bottomLeft().height(), newRadii.bottomRight().height());
        if (maxRadii > newRect.height()) {
            newRect.move(0, newRect.height() - maxRadii);
            newRect.setHeight(maxRadii);
        }
        break;

    case BoxSide::Left:
        overshoot = newRadii.topLeft().height() + newRadii.bottomLeft().height() - newRect.height();
        if (overshoot > 0) {
            ASSERT(!(newRadii.topLeft().height() && newRadii.bottomLeft().height()));
            newRect.setHeight(newRect.height() + overshoot);
            if (!newRadii.topLeft().height())
                newRect.move(0, -overshoot);
        }
        newRadii.setTopRight({ });
        newRadii.setBottomRight({ });
        maxRadii = std::max(newRadii.topLeft().width(), newRadii.bottomLeft().width());
        if (maxRadii > newRect.width())
            newRect.setWidth(maxRadii);
        break;

    case BoxSide::Right:
        overshoot = newRadii.topRight().height() + newRadii.bottomRight().height() - newRect.height();
        if (overshoot > 0) {
            ASSERT(!(newRadii.topRight().height() && newRadii.bottomRight().height()));
            newRect.setHeight(newRect.height() + overshoot);
            if (!newRadii.topRight().height())
                newRect.move(0, -overshoot);
        }
        newRadii.setTopLeft({ });
        newRadii.setBottomLeft({ });
        maxRadii = std::max(newRadii.topRight().width(), newRadii.bottomRight().width());
        if (maxRadii > newRect.width()) {
            newRect.move(newRect.width() - maxRadii, 0);
            newRect.setWidth(maxRadii);
        }
        break;
    }

    return FloatRoundedRect { newRect, newRadii };
}

bool BorderPainter::joinRequiresMitre(BoxSide side, BoxSide adjacentSide, bool allowOverdraw) const
{
    auto& edge = m_edges.at(side);
    auto& adjacentEdge = m_edges.at(adjacentSide);

    if ((edge.isTransparent() && adjacentEdge.isTransparent()) || !adjacentEdge.isPresent())
        return false;

    if (allowOverdraw && willBeOverdrawn(side, adjacentSide))
        return false;

    if (!edgesShareColor(edge, adjacentEdge))
        return true;

    if (borderStylesRequireMitre(side, adjacentSide, edge.style(), adjacentEdge.style()))
        return true;
    
    return false;
}

void BorderPainter::drawBoxSideFromPath(PaintingContext& paintingContext, const FloatRect& borderRect, const Path& borderPath, float thickness, float drawThickness, BoxSide side, Color color, BorderStyle borderStyle) const
{
    if (thickness <= 0)
        return;

    if (borderStyle == BorderStyle::Double && thickness < 3)
        borderStyle = BorderStyle::Solid;

    switch (borderStyle) {
    case BorderStyle::None:
    case BorderStyle::Hidden:
        return;
    case BorderStyle::Dotted:
    case BorderStyle::Dashed: {
        paintingContext.context.setStrokeColor(color);

        // The stroke is doubled here because the provided path is the
        // outside edge of the border so half the stroke is clipped off.
        // The extra multiplier is so that the clipping mask can antialias
        // the edges to prevent jaggies.
        paintingContext.context.setStrokeThickness(drawThickness * 2 * 1.1f);
        paintingContext.context.setStrokeStyle(borderStyle == BorderStyle::Dashed ? DashedStroke : DottedStroke);

        // If the number of dashes that fit in the path is odd and non-integral then we
        // will have an awkwardly-sized dash at the end of the path. To try to avoid that
        // here, we simply make the whitespace dashes ever so slightly bigger.
        // FIXME: This could be even better if we tried to manipulate the dash offset
        // and possibly the gapLength to get the corners dash-symmetrical.
        float dashLength = thickness * ((borderStyle == BorderStyle::Dashed) ? 3.0f : 1.0f);
        float gapLength = dashLength;
        float numberOfDashes = borderPath.length() / dashLength;
        // Don't try to show dashes if we have less than 2 dashes + 2 gaps.
        // FIXME: should do this test per side.
        if (numberOfDashes >= 4) {
            bool evenNumberOfFullDashes = !((int)numberOfDashes % 2);
            bool integralNumberOfDashes = !(numberOfDashes - (int)numberOfDashes);
            if (!evenNumberOfFullDashes && !integralNumberOfDashes) {
                float numberOfGaps = numberOfDashes / 2;
                gapLength += (dashLength  / numberOfGaps);
            }

            DashArray lineDash;
            lineDash.append(dashLength);
            lineDash.append(gapLength);
            paintingContext.context.setLineDash(lineDash, dashLength);
        }
        
        // FIXME: stroking the border path causes issues with tight corners:
        // https://bugs.webkit.org/show_bug.cgi?id=58711
        // Also, to get the best appearance we should stroke a path between the two borders.
        paintingContext.context.strokePath(borderPath);
        return;
    }
    case BorderStyle::Double: {
        // Get the inner border rects for both the outer border line and the inner border line
        // FIXME: Rect edges.
        float outerBorderTopWidth = m_edges.top().outerWidth();
        float innerBorderTopWidth = m_edges.top().innerWidth();

        float outerBorderRightWidth = m_edges.right().outerWidth();
        float innerBorderRightWidth = m_edges.right().innerWidth();

        float outerBorderBottomWidth = m_edges.bottom().outerWidth();
        float innerBorderBottomWidth = m_edges.bottom().innerWidth();

        float outerBorderLeftWidth = m_edges.left().outerWidth();
        float innerBorderLeftWidth = m_edges.left().innerWidth();

        // Draw inner border line
        {
            GraphicsContextStateSaver stateSaver(paintingContext.context);
            auto innerClip = roundedInsetBorderForRect(borderRect, m_borderRect.radii(), { innerBorderTopWidth, innerBorderRightWidth, innerBorderBottomWidth, innerBorderLeftWidth }, m_includeLeftEdge, m_includeRightEdge);
            
            paintingContext.context.clipRoundedRect(innerClip);
            drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, BorderStyle::Solid);
        }

        // Draw outer border line
        {
            GraphicsContextStateSaver stateSaver(paintingContext.context);
            auto outerRect = borderRect;
            if (m_bleedAvoidance == BackgroundBleedAvoidance::UseTransparencyLayer) {
                outerRect.inflate(1);
                ++outerBorderTopWidth;
                ++outerBorderBottomWidth;
                ++outerBorderLeftWidth;
                ++outerBorderRightWidth;
            }

            auto outerClip = roundedInsetBorderForRect(outerRect, m_borderRect.radii(), { outerBorderTopWidth, outerBorderRightWidth, outerBorderBottomWidth, outerBorderLeftWidth }, m_includeLeftEdge, m_includeRightEdge);
            paintingContext.context.clipOutRoundedRect(outerClip);
            drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, BorderStyle::Solid);
        }
        return;
    }
    case BorderStyle::Ridge:
    case BorderStyle::Groove:
    {
        BorderStyle s1;
        BorderStyle s2;
        if (borderStyle == BorderStyle::Groove) {
            s1 = BorderStyle::Inset;
            s2 = BorderStyle::Outset;
        } else {
            s1 = BorderStyle::Outset;
            s2 = BorderStyle::Inset;
        }
        
        // Paint full border
        drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, s1);

        // Paint inner only
        GraphicsContextStateSaver stateSaver(paintingContext.context);
        // FIXME: Should have pixel snapped these at display tree generation time.
        // FIXME: RectEdges
        float topWidth { m_edges.top().widthForPainting() / 2 };
        float bottomWidth { m_edges.bottom().widthForPainting() / 2 };
        float leftWidth { m_edges.left().widthForPainting() / 2 };
        float rightWidth { m_edges.right().widthForPainting() / 2 };

        auto clipRect = roundedInsetBorderForRect(borderRect, m_borderRect.radii(), { topWidth, rightWidth, bottomWidth, leftWidth }, m_includeLeftEdge, m_includeRightEdge);

        paintingContext.context.clipRoundedRect(clipRect);
        drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, s2);
        return;
    }
    case BorderStyle::Inset:
    case BorderStyle::Outset:
        calculateBorderStyleColor(borderStyle, side, color);
        break;
    default:
        break;
    }

    paintingContext.context.setStrokeStyle(NoStroke);
    paintingContext.context.setFillColor(color);
    paintingContext.context.drawRect(borderRect);
}

void BorderPainter::clipBorderSidePolygon(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, BoxSide side, bool firstEdgeMatches, bool secondEdgeMatches) const
{
    const FloatRect& outerRect = outerBorder.rect();
    const FloatRect& innerRect = innerBorder.rect();

    // For each side, create a quad that encompasses all parts of that side that may draw,
    // including areas inside the innerBorder.
    //
    //         0----------------3
    //       0  \              /  0
    //       |\  1----------- 2  /|
    //       | 1                1 |
    //       | |                | |
    //       | |                | |
    //       | 2                2 |
    //       |/  1------------2  \|
    //       3  /              \  3
    //         0----------------3
    //
    Vector<FloatPoint> quad;
    switch (side) {
    case BoxSide::Top:
        quad = { outerRect.minXMinYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMinYCorner(), outerRect.maxXMinYCorner() };

        if (!innerBorder.radii().topLeft().isZero())
            findIntersection(outerRect.minXMinYCorner(), innerRect.minXMinYCorner(), innerRect.minXMaxYCorner(), innerRect.maxXMinYCorner(), quad[1]);

        if (!innerBorder.radii().topRight().isZero())
            findIntersection(outerRect.maxXMinYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[2]);
        break;

    case BoxSide::Left:
        quad = { outerRect.minXMinYCorner(), innerRect.minXMinYCorner(), innerRect.minXMaxYCorner(), outerRect.minXMaxYCorner() };

        if (!innerBorder.radii().topLeft().isZero())
            findIntersection(outerRect.minXMinYCorner(), innerRect.minXMinYCorner(), innerRect.minXMaxYCorner(), innerRect.maxXMinYCorner(), quad[1]);

        if (!innerBorder.radii().bottomLeft().isZero())
            findIntersection(outerRect.minXMaxYCorner(), innerRect.minXMaxYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[2]);
        break;

    case BoxSide::Bottom:
        quad = { outerRect.minXMaxYCorner(), innerRect.minXMaxYCorner(), innerRect.maxXMaxYCorner(), outerRect.maxXMaxYCorner() };

        if (!innerBorder.radii().bottomLeft().isZero())
            findIntersection(outerRect.minXMaxYCorner(), innerRect.minXMaxYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[1]);

        if (!innerBorder.radii().bottomRight().isZero())
            findIntersection(outerRect.maxXMaxYCorner(), innerRect.maxXMaxYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMaxYCorner(), quad[2]);
        break;

    case BoxSide::Right:
        quad = { outerRect.maxXMinYCorner(), innerRect.maxXMinYCorner(), innerRect.maxXMaxYCorner(), outerRect.maxXMaxYCorner() };

        if (!innerBorder.radii().topRight().isZero())
            findIntersection(outerRect.maxXMinYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[1]);

        if (!innerBorder.radii().bottomRight().isZero())
            findIntersection(outerRect.maxXMaxYCorner(), innerRect.maxXMaxYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMaxYCorner(), quad[2]);
        break;
    }

    // If the border matches both of its adjacent sides, don't anti-alias the clip, and
    // if neither side matches, anti-alias the clip.
    if (firstEdgeMatches == secondEdgeMatches) {
        bool wasAntialiased = paintingContext.context.shouldAntialias();
        paintingContext.context.setShouldAntialias(!firstEdgeMatches);
        paintingContext.context.clipPath(Path::polygonPathFromPoints(quad), WindRule::NonZero);
        paintingContext.context.setShouldAntialias(wasAntialiased);
        return;
    }

    // Square off the end which shouldn't be affected by antialiasing, and clip.
    Vector<FloatPoint> firstQuad = {
        quad[0],
        quad[1],
        quad[2],
        side == BoxSide::Top || side == BoxSide::Bottom ? FloatPoint(quad[3].x(), quad[2].y()) : FloatPoint(quad[2].x(), quad[3].y()),
        quad[3]
    };
    bool wasAntialiased = paintingContext.context.shouldAntialias();
    paintingContext.context.setShouldAntialias(!firstEdgeMatches);
    paintingContext.context.clipPath(Path::polygonPathFromPoints(firstQuad), WindRule::NonZero);

    Vector<FloatPoint> secondQuad = {
        quad[0],
        side == BoxSide::Top || side == BoxSide::Bottom ? FloatPoint(quad[0].x(), quad[1].y()) : FloatPoint(quad[1].x(), quad[0].y()),
        quad[1],
        quad[2],
        quad[3]
    };
    // Antialiasing affects the second side.
    paintingContext.context.setShouldAntialias(!secondEdgeMatches);
    paintingContext.context.clipPath(Path::polygonPathFromPoints(secondQuad), WindRule::NonZero);

    paintingContext.context.setShouldAntialias(wasAntialiased);
}

void BorderPainter::drawLineForBoxSide(PaintingContext& paintingContext, const FloatRect& rect, BoxSide side, Color color, BorderStyle borderStyle, float adjacentWidth1, float adjacentWidth2, bool antialias) const
{
    auto drawBorderRect = [&](const FloatRect& rect) {
        if (rect.isEmpty())
            return;
        paintingContext.context.drawRect(rect);
    };

    auto drawLineFor = [&](const FloatRect& rect, BoxSide side, BorderStyle borderStyle, const FloatSize& adjacent) {
        if (rect.isEmpty())
            return;
        drawLineForBoxSide(paintingContext, rect, side, color, borderStyle, adjacent.width(), adjacent.height(), antialias);
    };
    
    auto& edge = m_edges.at(side);

    const float deviceScaleFactor = paintingContext.deviceScaleFactor;
    float x1 = rect.x();
    float x2 = rect.maxX();
    float y1 = rect.y();
    float y2 = rect.maxY();
    float thickness;
    float length;
    if (side == BoxSide::Top || side == BoxSide::Bottom) {
        thickness = y2 - y1;
        length = x2 - x1;
    } else {
        thickness = x2 - x1;
        length = y2 - y1;
    }
    // FIXME: We really would like this check to be an ASSERT as we don't want to draw empty borders. However
    // nothing guarantees that the following recursive calls to drawLineForBoxSide will have non-null dimensions.
    if (!thickness || !length)
        return;

    switch (borderStyle) {
    case BorderStyle::None:
    case BorderStyle::Hidden:
        return;
    case BorderStyle::Dotted:
    case BorderStyle::Dashed: {
        bool wasAntialiased = paintingContext.context.shouldAntialias();
        StrokeStyle oldStrokeStyle = paintingContext.context.strokeStyle();
        paintingContext.context.setShouldAntialias(antialias);
        paintingContext.context.setStrokeColor(color);
        paintingContext.context.setStrokeThickness(thickness);
        paintingContext.context.setStrokeStyle(borderStyle == BorderStyle::Dashed ? DashedStroke : DottedStroke);
        paintingContext.context.drawLine({ x1, y1 }, { x2, y2 });
        paintingContext.context.setShouldAntialias(wasAntialiased);
        paintingContext.context.setStrokeStyle(oldStrokeStyle);
        break;
    }
    case BorderStyle::Double: {
        float thirdOfThickness = ceilToDevicePixel(thickness / 3, deviceScaleFactor);
        if (!adjacentWidth1 && !adjacentWidth2) {
            StrokeStyle oldStrokeStyle = paintingContext.context.strokeStyle();
            paintingContext.context.setStrokeStyle(NoStroke);
            paintingContext.context.setFillColor(color);

            bool wasAntialiased = paintingContext.context.shouldAntialias();
            paintingContext.context.setShouldAntialias(antialias);

            switch (side) {
            case BoxSide::Top:
                drawBorderRect({ x1, y1, length, edge.outerWidth() });
                drawBorderRect({ x1, y2 - edge.innerWidth(), length, edge.innerWidth() });
                break;
            case BoxSide::Bottom:
                drawBorderRect({ x1, y2 - edge.outerWidth(), length, edge.outerWidth() });
                drawBorderRect({ x1, y1, length, edge.innerWidth() });
                break;
            case BoxSide::Left:
                drawBorderRect({ x1, y1, edge.outerWidth(), length });
                drawBorderRect({ x2 - edge.innerWidth(), y1, edge.innerWidth(), length });
                break;
            case BoxSide::Right:
                drawBorderRect({ x2 - edge.outerWidth(), y1, edge.outerWidth(), length });
                drawBorderRect({ x1, y1, edge.innerWidth(), length });
                break;
            }

            paintingContext.context.setShouldAntialias(wasAntialiased);
            paintingContext.context.setStrokeStyle(oldStrokeStyle);
        } else {
            float adjacent1BigThird = ceilToDevicePixel(adjacentWidth1 / 3, deviceScaleFactor);
            float adjacent2BigThird = ceilToDevicePixel(adjacentWidth2 / 3, deviceScaleFactor);

            float offset1 = floorToDevicePixel(fabs(adjacentWidth1) * 2 / 3, deviceScaleFactor);
            float offset2 = floorToDevicePixel(fabs(adjacentWidth2) * 2 / 3, deviceScaleFactor);

            float mitreOffset1 = adjacentWidth1 < 0 ? offset1 : 0;
            float mitreOffset2 = adjacentWidth1 > 0 ? offset1 : 0;
            float mitreOffset3 = adjacentWidth2 < 0 ? offset2 : 0;
            float mitreOffset4 = adjacentWidth2 > 0 ? offset2 : 0;

            FloatRect paintBorderRect;
            switch (side) {
            case BoxSide::Top:
                paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset1, y1, (x2 - mitreOffset3) - (x1 + mitreOffset1), thirdOfThickness), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));

                paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset2, y2 - thirdOfThickness, (x2 - mitreOffset4) - (x1 + mitreOffset2), thirdOfThickness), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
                break;
            case BoxSide::Left:
                paintBorderRect = snapRectToDevicePixels(LayoutRect(x1, y1 + mitreOffset1, thirdOfThickness, (y2 - mitreOffset3) - (y1 + mitreOffset1)), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));

                paintBorderRect = snapRectToDevicePixels(LayoutRect(x2 - thirdOfThickness, y1 + mitreOffset2, thirdOfThickness, (y2 - mitreOffset4) - (y1 + mitreOffset2)), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
                break;
            case BoxSide::Bottom:
                paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset2, y1, (x2 - mitreOffset4) - (x1 + mitreOffset2), thirdOfThickness), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));

                paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset1, y2 - thirdOfThickness, (x2 - mitreOffset3) - (x1 + mitreOffset1), thirdOfThickness), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
                break;
            case BoxSide::Right:
                paintBorderRect = snapRectToDevicePixels(LayoutRect(x1, y1 + mitreOffset2, thirdOfThickness, (y2 - mitreOffset4) - (y1 + mitreOffset2)), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));

                paintBorderRect = snapRectToDevicePixels(LayoutRect(x2 - thirdOfThickness, y1 + mitreOffset1, thirdOfThickness, (y2 - mitreOffset3) - (y1 + mitreOffset1)), deviceScaleFactor);
                drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
                break;
            }
        }
        break;
    }
    case BorderStyle::Ridge:
    case BorderStyle::Groove: {
        BorderStyle s1;
        BorderStyle s2;
        if (borderStyle == BorderStyle::Groove) {
            s1 = BorderStyle::Inset;
            s2 = BorderStyle::Outset;
        } else {
            s1 = BorderStyle::Outset;
            s2 = BorderStyle::Inset;
        }

        float adjacent1BigHalf = ceilToDevicePixel(adjacentWidth1 / 2, deviceScaleFactor);
        float adjacent2BigHalf = ceilToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);

        float adjacent1SmallHalf = floorToDevicePixel(adjacentWidth1 / 2, deviceScaleFactor);
        float adjacent2SmallHalf = floorToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);

        float offset1 = 0;
        float offset2 = 0;
        float offset3 = 0;
        float offset4 = 0;

        if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth1 < 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth1 > 0))
            offset1 = floorToDevicePixel(adjacentWidth1 / 2, deviceScaleFactor);

        if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth2 < 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth2 > 0))
            offset2 = ceilToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);

        if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth1 > 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth1 < 0))
            offset3 = floorToDevicePixel(fabs(adjacentWidth1) / 2, deviceScaleFactor);

        if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth2 > 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth2 < 0))
            offset4 = ceilToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);

        float adjustedX = ceilToDevicePixel((x1 + x2) / 2, deviceScaleFactor);
        float adjustedY = ceilToDevicePixel((y1 + y2) / 2, deviceScaleFactor);
        // Quads can't use the default snapping rect functions.
        x1 = roundToDevicePixel(x1, deviceScaleFactor);
        x2 = roundToDevicePixel(x2, deviceScaleFactor);
        y1 = roundToDevicePixel(y1, deviceScaleFactor);
        y2 = roundToDevicePixel(y2, deviceScaleFactor);

        switch (side) {
        case BoxSide::Top:
            drawLineFor(FloatRect(FloatPoint(x1 + offset1, y1), FloatPoint(x2 - offset2, adjustedY)), side, s1, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
            drawLineFor(FloatRect(FloatPoint(x1 + offset3, adjustedY), FloatPoint(x2 - offset4, y2)), side, s2, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
            break;
        case BoxSide::Left:
            drawLineFor(FloatRect(FloatPoint(x1, y1 + offset1), FloatPoint(adjustedX, y2 - offset2)), side, s1, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
            drawLineFor(FloatRect(FloatPoint(adjustedX, y1 + offset3), FloatPoint(x2, y2 - offset4)), side, s2, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
            break;
        case BoxSide::Bottom:
            drawLineFor(FloatRect(FloatPoint(x1 + offset1, y1), FloatPoint(x2 - offset2, adjustedY)), side, s2, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
            drawLineFor(FloatRect(FloatPoint(x1 + offset3, adjustedY), FloatPoint(x2 - offset4, y2)), side, s1, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
            break;
        case BoxSide::Right:
            drawLineFor(FloatRect(FloatPoint(x1, y1 + offset1), FloatPoint(adjustedX, y2 - offset2)), side, s2, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
            drawLineFor(FloatRect(FloatPoint(adjustedX, y1 + offset3), FloatPoint(x2, y2 - offset4)), side, s1, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
            break;
        }
        break;
    }
    case BorderStyle::Inset:
    case BorderStyle::Outset:
        calculateBorderStyleColor(borderStyle, side, color);
        FALLTHROUGH;
    case BorderStyle::Solid: {
        StrokeStyle oldStrokeStyle = paintingContext.context.strokeStyle();
        ASSERT(x2 >= x1);
        ASSERT(y2 >= y1);
        if (!adjacentWidth1 && !adjacentWidth2) {
            paintingContext.context.setStrokeStyle(NoStroke);
            paintingContext.context.setFillColor(color);
            bool wasAntialiased = paintingContext.context.shouldAntialias();
            paintingContext.context.setShouldAntialias(antialias);
            drawBorderRect(snapRectToDevicePixels(LayoutRect(x1, y1, x2 - x1, y2 - y1), deviceScaleFactor));
            paintingContext.context.setShouldAntialias(wasAntialiased);
            paintingContext.context.setStrokeStyle(oldStrokeStyle);
            return;
        }

        // FIXME: These roundings should be replaced by ASSERT(device pixel positioned) when all the callers have transitioned to device pixels.
        x1 = roundToDevicePixel(x1, deviceScaleFactor);
        y1 = roundToDevicePixel(y1, deviceScaleFactor);
        x2 = roundToDevicePixel(x2, deviceScaleFactor);
        y2 = roundToDevicePixel(y2, deviceScaleFactor);

        Vector<FloatPoint> quad;
        switch (side) {
        case BoxSide::Top:
            quad = {
                { x1 + std::max<float>(-adjacentWidth1, 0), y1 },
                { x1 + std::max<float>(adjacentWidth1, 0), y2 },
                { x2 - std::max<float>(adjacentWidth2, 0), y2 },
                { x2 - std::max<float>(-adjacentWidth2, 0), y1 }
            };
            break;
        case BoxSide::Bottom:
            quad = {
                { x1 + std::max<float>(adjacentWidth1, 0), y1 },
                { x1 + std::max<float>(-adjacentWidth1, 0), y2 },
                { x2 - std::max<float>(-adjacentWidth2, 0), y2 },
                { x2 - std::max<float>(adjacentWidth2, 0), y1 }
            };
            break;
        case BoxSide::Left:
            quad = {
                { x1, y1 + std::max<float>(-adjacentWidth1, 0) },
                { x1, y2 - std::max<float>(-adjacentWidth2, 0) },
                { x2, y2 - std::max<float>(adjacentWidth2, 0) },
                { x2, y1 + std::max<float>(adjacentWidth1, 0) }
            };
            break;
        case BoxSide::Right:
            quad = {
                { x1, y1 + std::max<float>(adjacentWidth1, 0) },
                { x1, y2 - std::max<float>(adjacentWidth2, 0) },
                { x2, y2 - std::max<float>(-adjacentWidth2, 0) },
                { x2, y1 + std::max<float>(-adjacentWidth1, 0) }
            };
            break;
        }

        paintingContext.context.setStrokeStyle(NoStroke);
        paintingContext.context.setFillColor(color);
        bool wasAntialiased = paintingContext.context.shouldAntialias();
        paintingContext.context.setShouldAntialias(antialias);
        paintingContext.context.fillPath(Path::polygonPathFromPoints(quad));
        paintingContext.context.setShouldAntialias(wasAntialiased);

        paintingContext.context.setStrokeStyle(oldStrokeStyle);
        break;
    }
    }
}

void BorderPainter::paintOneBorderSide(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, const FloatRect& sideRect, BoxSide side, const Path* path, bool antialias, const Color* overrideColor) const
{
    auto& edgeToRender = m_edges.at(side);
    ASSERT(edgeToRender.widthForPainting());
    
    auto adjacentSides = adjacentSidesForSide(side);
    
    auto& adjacentEdge1 = m_edges.at(adjacentSides.first);
    auto& adjacentEdge2 = m_edges.at(adjacentSides.second);

    bool mitreAdjacentSide1 = joinRequiresMitre(side, adjacentSides.first, !antialias);
    bool mitreAdjacentSide2 = joinRequiresMitre(side, adjacentSides.second, !antialias);
    
    bool adjacentSide1StylesMatch = colorsMatchAtCorner(side, adjacentSides.first);
    bool adjacentSide2StylesMatch = colorsMatchAtCorner(side, adjacentSides.second);

    const Color& colorToPaint = overrideColor ? *overrideColor : edgeToRender.color();

    if (path) {
        GraphicsContextStateSaver stateSaver(paintingContext.context);

        clipBorderSidePolygon(paintingContext, outerBorder, innerBorder, side, adjacentSide1StylesMatch, adjacentSide2StylesMatch);

        if (!innerBorder.isRenderable())
            paintingContext.context.clipOutRoundedRect(FloatRoundedRect(calculateAdjustedInnerBorder(innerBorder, side)));

        float thickness = std::max(std::max(edgeToRender.widthForPainting(), adjacentEdge1.widthForPainting()), adjacentEdge2.widthForPainting());
        drawBoxSideFromPath(paintingContext, outerBorder.rect(), *path, edgeToRender.widthForPainting(), thickness, side, colorToPaint, edgeToRender.style());
        return;
    }

    bool clipForStyle = styleRequiresClipPolygon(edgeToRender.style()) && (mitreAdjacentSide1 || mitreAdjacentSide2);
    bool clipAdjacentSide1 = colorNeedsAntiAliasAtCorner(side, adjacentSides.first) && mitreAdjacentSide1;
    bool clipAdjacentSide2 = colorNeedsAntiAliasAtCorner(side, adjacentSides.second) && mitreAdjacentSide2;
    bool shouldClip = clipForStyle || clipAdjacentSide1 || clipAdjacentSide2;
    
    GraphicsContextStateSaver clipStateSaver(paintingContext.context, shouldClip);
    if (shouldClip) {
        bool aliasAdjacentSide1 = clipAdjacentSide1 || (clipForStyle && mitreAdjacentSide1);
        bool aliasAdjacentSide2 = clipAdjacentSide2 || (clipForStyle && mitreAdjacentSide2);
        clipBorderSidePolygon(paintingContext, outerBorder, innerBorder, side, !aliasAdjacentSide1, !aliasAdjacentSide2);
        // Since we clipped, no need to draw with a mitre.
        mitreAdjacentSide1 = false;
        mitreAdjacentSide2 = false;
    }
    drawLineForBoxSide(paintingContext, sideRect, side, colorToPaint, edgeToRender.style(), mitreAdjacentSide1 ? adjacentEdge1.widthForPainting() : 0, mitreAdjacentSide2 ? adjacentEdge2.widthForPainting() : 0, antialias);
}

void BorderPainter::paintBorderSides(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet edgeSet, bool antialias, const Color* overrideColor) const
{
    bool renderRadii = outerBorder.isRounded();

    Path roundedPath;
    if (renderRadii)
        roundedPath.addRoundedRect(outerBorder);

    auto paintOneSide = [&](BoxSide side) {
        auto& edge = m_edges.at(side);
        if (!edge.shouldRender() || !edgeSet.contains(edgeFlagForSide(side)))
            return;

        auto sideRect = outerBorder.rect();
        FloatSize firstRadius;
        FloatSize secondRadius;

        switch (side) {
        case BoxSide::Top:
            sideRect.setHeight(edge.widthForPainting() + innerBorderBleedAdjustment.height());
            firstRadius = innerBorder.radii().topLeft();
            secondRadius = innerBorder.radii().topRight();
            break;
        case BoxSide::Right:
            sideRect.shiftXEdgeTo(sideRect.maxX() - edge.widthForPainting() - innerBorderBleedAdjustment.width());
            firstRadius = innerBorder.radii().bottomRight();
            secondRadius = innerBorder.radii().topRight();
            break;
        case BoxSide::Bottom:
            sideRect.shiftYEdgeTo(sideRect.maxY() - edge.widthForPainting() - innerBorderBleedAdjustment.height());
            firstRadius = innerBorder.radii().bottomLeft();
            secondRadius = innerBorder.radii().bottomRight();
            break;
        case BoxSide::Left:
            sideRect.setWidth(edge.widthForPainting() + innerBorderBleedAdjustment.width());
            firstRadius = innerBorder.radii().bottomLeft();
            secondRadius = innerBorder.radii().topLeft();
            break;
        }

        bool usePath = renderRadii && (borderStyleHasInnerDetail(edge.style()) || borderWillArcInnerEdge(firstRadius, secondRadius));
        paintOneBorderSide(paintingContext, outerBorder, innerBorder, sideRect, side, usePath ? &roundedPath : nullptr, antialias, overrideColor);
    };

    paintOneSide(BoxSide::Top);
    paintOneSide(BoxSide::Bottom);
    paintOneSide(BoxSide::Left);
    paintOneSide(BoxSide::Right);
}

void BorderPainter::paintTranslucentBorderSides(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet edgesToDraw, bool antialias) const
{
    // willBeOverdrawn assumes that we draw in order: top, bottom, left, right.
    // This is different from BoxSide enum order.
    static constexpr std::array<BoxSide, 4> paintOrderSides = { BoxSide::Top, BoxSide::Bottom, BoxSide::Left, BoxSide::Right };

    while (edgesToDraw) {
        // Find undrawn edges sharing a color.
        Color commonColor;
        
        BoxSideSet commonColorEdgeSet;
        for (auto side : paintOrderSides) {
            if (!edgesToDraw.contains(edgeFlagForSide(side)))
                continue;

            auto& edge = m_edges.at(side);
            bool includeEdge;
            if (commonColorEdgeSet.isEmpty()) {
                commonColor = edge.color();
                includeEdge = true;
            } else
                includeEdge = edge.color() == commonColor;

            if (includeEdge)
                commonColorEdgeSet.add(edgeFlagForSide(side));
        }

        bool useTransparencyLayer = includesAdjacentEdges(commonColorEdgeSet) && !commonColor.isOpaque();
        {
            auto transparencyScope = TransparencyLayerScope { paintingContext.context, commonColor.alphaAsFloat(), useTransparencyLayer };
            if (useTransparencyLayer)
                commonColor = commonColor.opaqueColor();

            paintBorderSides(paintingContext, outerBorder, innerBorder, innerBorderBleedAdjustment, commonColorEdgeSet, antialias, &commonColor);
        }
        
        edgesToDraw.remove(commonColorEdgeSet);
    }
}

static FloatRect shrinkRectByOneDevicePixel(const PaintingContext& paintingContext, const FloatRect& rect)
{
    auto shrunkRect = rect;
    auto transform = paintingContext.context.getCTM();
    shrunkRect.inflateX(-ceilToDevicePixel(1.0f / transform.xScale(), paintingContext.deviceScaleFactor));
    shrunkRect.inflateY(-ceilToDevicePixel(1.0f / transform.yScale(), paintingContext.deviceScaleFactor));
    return shrunkRect;
}

FloatRect BorderPainter::borderInnerRectAdjustedForBleedAvoidance(const PaintingContext& paintingContext) const
{
    if (m_bleedAvoidance != BackgroundBleedAvoidance::BackgroundOverBorder)
        return m_borderRect.rect();

    // We shrink the rectangle by one device pixel on each side to make it fully overlap the anti-aliased background border
    return shrinkRectByOneDevicePixel(paintingContext, m_borderRect.rect());
}

void BorderPainter::paintBorders(PaintingContext& paintingContext) const
{
    bool haveAlphaColor = false;
    bool haveAllSolidEdges = true;
    bool haveAllDoubleEdges = true;
    int numEdgesVisible = 4;
    bool allEdgesShareColor = true;
    std::optional<BoxSide> firstVisibleSide;
    BoxSideSet edgesToDraw;

    for (auto side : allBoxSides) {
        auto& currEdge = m_edges.at(side);

        if (currEdge.shouldRender())
            edgesToDraw.add(edgeFlagForSide(side));

        if (currEdge.presentButInvisible()) {
            --numEdgesVisible;
            allEdgesShareColor = false;
            continue;
        }
        
        if (!currEdge.widthForPainting()) {
            --numEdgesVisible;
            continue;
        }

        if (!firstVisibleSide)
            firstVisibleSide = side;
        else if (currEdge.color() != m_edges.at(*firstVisibleSide).color())
            allEdgesShareColor = false;

        if (!currEdge.color().isOpaque())
            haveAlphaColor = true;
        
        if (currEdge.style() != BorderStyle::Solid)
            haveAllSolidEdges = false;

        if (currEdge.style() != BorderStyle::Double)
            haveAllDoubleEdges = false;
    }

    FloatRoundedRect innerBorderRect = roundedInsetBorderForRect(borderInnerRectAdjustedForBleedAvoidance(paintingContext), m_borderRect.radii(), borderWidths(m_edges), m_includeLeftEdge, m_includeRightEdge);

    // FIXME: If no corner intersects the clip region, we can pretend outerBorder is rectangular to improve performance.
    
    if ((haveAllSolidEdges || haveAllDoubleEdges) && allEdgesShareColor && innerBorderRect.isRenderable()) {
        // Fast path for drawing all solid edges and all unrounded double edges
        if (numEdgesVisible == 4 && (m_borderRect.isRounded() || haveAlphaColor) && (haveAllSolidEdges || (!m_borderRect.isRounded() && !innerBorderRect.isRounded()))) {
            Path path;
            
            if (m_borderRect.isRounded() && m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer)
                path.addRoundedRect(m_borderRect);
            else
                path.addRect(m_borderRect.rect());

            if (haveAllDoubleEdges) {
                auto innerThirdRect = m_borderRect.rect();
                auto outerThirdRect = m_borderRect.rect();

                for (auto side : allBoxSides) {
                    auto& edge = m_edges.at(side);
                    float outerWidth = edge.outerWidth();
                    float innerWidth = edge.innerWidth();
                    switch (side) {
                    case BoxSide::Top:
                        innerThirdRect.shiftYEdgeTo(innerThirdRect.y() + innerWidth);
                        outerThirdRect.shiftYEdgeTo(outerThirdRect.y() + outerWidth);
                        break;
                    case BoxSide::Right:
                        innerThirdRect.setWidth(innerThirdRect.width() - innerWidth);
                        outerThirdRect.setWidth(outerThirdRect.width() - outerWidth);
                        break;
                    case BoxSide::Bottom:
                        innerThirdRect.setHeight(innerThirdRect.height() - innerWidth);
                        outerThirdRect.setHeight(outerThirdRect.height() - outerWidth);
                        break;
                    case BoxSide::Left:
                        innerThirdRect.shiftXEdgeTo(innerThirdRect.x() + innerWidth);
                        outerThirdRect.shiftXEdgeTo(outerThirdRect.x() + outerWidth);
                        break;
                    }
                }

                auto outerThirdRoundedRect = m_borderRect;
                outerThirdRoundedRect.setRect(outerThirdRect);

                if (outerThirdRoundedRect.isRounded() && m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer)
                    path.addRoundedRect(outerThirdRoundedRect);
                else
                    path.addRect(outerThirdRoundedRect.rect());

                auto innerThirdRoundedRect = innerBorderRect;
                innerThirdRoundedRect.setRect(innerThirdRect);
                if (innerThirdRoundedRect.isRounded())
                    path.addRoundedRect(innerThirdRoundedRect);
                else
                    path.addRect(innerThirdRoundedRect.rect());
            }

            if (innerBorderRect.isRounded())
                path.addRoundedRect(innerBorderRect);
            else
                path.addRect(innerBorderRect.rect());
            
            paintingContext.context.setFillRule(WindRule::EvenOdd);
            paintingContext.context.setFillColor(m_edges.top().color());
            paintingContext.context.fillPath(path);
            return;
        }

        // Avoid creating transparent layers
        if (haveAllSolidEdges && numEdgesVisible != 4 && !m_borderRect.isRounded() && haveAlphaColor) {
            Path path;

            auto calculateSideRect = [&](const BorderEdge& edge, BoxSide side) {
                auto sideRect = m_borderRect.rect();
                auto width = edge.widthForPainting();

                switch (side) {
                case BoxSide::Top:
                    sideRect.setHeight(width);
                    break;
                case BoxSide::Right:
                    sideRect.shiftXEdgeTo(sideRect.maxX() - width);
                    break;
                case BoxSide::Bottom:
                    sideRect.shiftYEdgeTo(sideRect.maxY() - width);
                    break;
                case BoxSide::Left:
                    sideRect.setWidth(width);
                    break;
                }

                return sideRect;
            };

            for (auto side : allBoxSides) {
                auto& edge = m_edges.at(side);
                if (edge.shouldRender()) {
                    auto sideRect = calculateSideRect(edge, side);
                    path.addRect(sideRect);
                }
            }

            paintingContext.context.setFillRule(WindRule::NonZero);
            paintingContext.context.setFillColor(m_edges.at(*firstVisibleSide).color());
            paintingContext.context.fillPath(path);
            return;
        }
    }

    bool clipToOuterBorder = m_borderRect.isRounded();
    GraphicsContextStateSaver stateSaver(paintingContext.context, clipToOuterBorder);
    if (clipToOuterBorder) {
        // Clip to the inner and outer radii rects.
        if (m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer)
            paintingContext.context.clipRoundedRect(m_borderRect);

        if (innerBorderRect.isRenderable())
            paintingContext.context.clipOutRoundedRect(innerBorderRect);
    }

    bool shouldAntialiasLines = !paintingContext.context.getCTM().isIdentityOrTranslationOrFlipped();

    // If only one edge visible antialiasing doesn't create seams
    bool antialias = shouldAntialiasLines || numEdgesVisible == 1;

    auto unadjustedInnerBorder = innerBorderRect;
    FloatSize innerBorderBleedAdjustment;
    if (m_bleedAvoidance == BackgroundBleedAvoidance::BackgroundOverBorder) {
        unadjustedInnerBorder = roundedInsetBorderForRect(m_borderRect.rect(), m_borderRect.radii(), borderWidths(m_edges), m_includeLeftEdge, m_includeRightEdge);
        innerBorderBleedAdjustment = innerBorderRect.rect().location() - unadjustedInnerBorder.rect().location();
    }

    if (haveAlphaColor)
        paintTranslucentBorderSides(paintingContext, m_borderRect, innerBorderRect, innerBorderBleedAdjustment, edgesToDraw, antialias); // FIXME.
    else
        paintBorderSides(paintingContext, m_borderRect, innerBorderRect, innerBorderBleedAdjustment, edgesToDraw, antialias);
}

BoxDecorationPainter::BoxDecorationPainter(const BoxModelBox& box, PaintingContext& paintingContext, bool includeLeftEdge, bool includeRightEdge)
    : m_box(box)
    , m_borderRect(box.borderRoundedRect())
    , m_bleedAvoidance(determineBackgroundBleedAvoidance(box, paintingContext))
    , m_includeLeftEdge(includeLeftEdge)
    , m_includeRightEdge(includeRightEdge)
{
}

void BoxDecorationPainter::paintBorders(PaintingContext& paintingContext) const
{
    auto* boxDecorationData = m_box.boxDecorationData();
    if (!boxDecorationData)
        return;

    if (boxDecorationData->hasBorderImage()) {
        // border-image is not affected by border-radius.
        // FIXME: Implement border-image painting.
        return;
    }

    auto outerBorder = borderRoundedRect();
    BorderPainter painter(boxDecorationData->borderEdges(), outerBorder, m_bleedAvoidance, m_includeLeftEdge, m_includeRightEdge);
    painter.paintBorders(paintingContext);
}

void BoxDecorationPainter::paintFillLayer(PaintingContext& paintingContext, const FillLayer& layer, const FillLayerImageGeometry& geometry) const
{
    GraphicsContextStateSaver stateSaver(paintingContext.context, false);

    auto clipRectForLayer = [](const BoxModelBox& box, const FillLayer& layer) -> UnadjustedAbsoluteFloatRect {
        switch (layer.clip()) {
        case FillBox::Border:
            return box.absoluteBorderBoxRect();
        case FillBox::Padding:
            return box.absolutePaddingBoxRect();
        case FillBox::Content:
            return box.absoluteContentBoxRect();
        case FillBox::Text:
        case FillBox::NoClip:
            break;
        }
        return { };
    };

    switch (layer.clip()) {
    case FillBox::Border:
    case FillBox::Padding:
    case FillBox::Content: {
        stateSaver.save();
        paintingContext.context.clip(clipRectForLayer(m_box, layer));
        break;
    }
    case FillBox::Text:
    case FillBox::NoClip:
        break;
    }

    // FIXME: Handle background compositing modes.

    auto* backgroundImage = layer.image();
    CompositeOperator op = CompositeOperator::SourceOver;

    if (geometry.destRect().isEmpty())
        return;

    auto image = backgroundImage->image(nullptr, geometry.tileSize());
    if (!image)
        return;

    // FIXME: call image->updateFromSettings().

    ImagePaintingOptions options = {
        op == CompositeOperator::SourceOver ? layer.composite() : op,
        layer.blendMode(),
        DecodingMode::Synchronous,
        ImageOrientation::FromImage,
        InterpolationQuality::Default
    };

    paintingContext.context.drawTiledImage(*image, geometry.destRect(), toFloatPoint(geometry.relativePhase()), geometry.tileSize(), geometry.spaceSize(), options);
}

void BoxDecorationPainter::paintBoxShadow(PaintingContext& paintingContext, ShadowStyle shadowStyle) const
{
    if (!m_box.style().boxShadow())
        return;

    auto borderRect = shadowStyle == ShadowStyle::Inset ? innerBorderRoundedRect() : borderRoundedRect();
    auto* boxDecorationData = m_box.boxDecorationData();
    bool hasBorderRadius = m_box.hasBorderRadius();

    bool hasOpaqueBackground = m_box.style().backgroundColor().isOpaque();

    auto paintNormalShadow = [&](const ShadowData& shadow) {
        // FIXME: Snapping here isn't ideal. It would be better to compute a rect which is border rect + offset + spread, and snap that at tree building time.
        auto shadowOffset = roundSizeToDevicePixels({ shadow.x().value(), shadow.y().value() }, paintingContext.deviceScaleFactor);
        float shadowPaintingExtent = ceilToDevicePixel(shadow.paintingExtent(), paintingContext.deviceScaleFactor);
        float shadowSpread = roundToDevicePixel(shadow.spread().value(), paintingContext.deviceScaleFactor);
        auto shadowRadius = shadow.radius();

        auto fillRect = borderRect;
        fillRect.inflate(shadowSpread);
        if (fillRect.isEmpty())
            return;

        auto shadowRect = borderRect.rect();
        shadowRect.inflate(shadowPaintingExtent + shadowSpread);
        shadowRect.move(shadowOffset);

        GraphicsContextStateSaver stateSaver(paintingContext.context);
        paintingContext.context.clip(shadowRect);

        // Move the fill just outside the clip, adding at least 1 pixel of separation so that the fill does not
        // bleed in (due to antialiasing) if the context is transformed.
        float xOffset = shadowRect.width() + std::max<float>(0, shadowOffset.width()) + shadowPaintingExtent + 2 * shadowSpread + 1.0f;
        auto extraOffset = FloatSize { std::ceil(xOffset), 0 };
        shadowOffset -= extraOffset;
        fillRect.move(extraOffset);

        auto rectToClipOut = borderRect;
        auto adjustedFillRect = fillRect;

        auto shadowRectOrigin = fillRect.rect().location() + shadowOffset;
        auto adjustedShadowOffset = shadowRectOrigin - adjustedFillRect.rect().location();

        paintingContext.context.setShadow(adjustedShadowOffset, shadowRadius.value(), shadow.color(), shadow.isWebkitBoxShadow() ? ShadowRadiusMode::Legacy : ShadowRadiusMode::Default);

        if (hasBorderRadius) {
            // If the box is opaque, it is unnecessary to clip it out. However, doing so saves time
            // when painting the shadow. On the other hand, it introduces subpixel gaps along the
            // corners. Those are avoided by insetting the clipping path by one pixel.
            if (hasOpaqueBackground)
                rectToClipOut.inflateWithRadii(-1.0f);

            if (!rectToClipOut.isEmpty())
                paintingContext.context.clipOutRoundedRect(rectToClipOut);

            auto influenceRect = FloatRoundedRect { shadowRect, borderRect.radii() };
            influenceRect.expandRadii(2 * shadowPaintingExtent + shadowSpread);

            // FIXME: Optimize for clipped-out corners
            adjustedFillRect.expandRadii(shadowSpread);
            if (!adjustedFillRect.isRenderable())
                adjustedFillRect.adjustRadii();
            paintingContext.context.fillRoundedRect(adjustedFillRect, Color::black);
        } else {
            // If the box is opaque, it is unnecessary to clip it out. However, doing so saves time
            // when painting the shadow. On the other hand, it introduces subpixel gaps along the
            // edges if they are not pixel-aligned. Those are avoided by insetting the clipping path
            // by one pixel.
            if (hasOpaqueBackground) {
                // FIXME: The function to decide on the policy based on the transform should be a named function.
                // FIXME: It's not clear if this check is right. What about integral scale factors?
                AffineTransform transform = paintingContext.context.getCTM();
                if (transform.a() != 1 || (transform.d() != 1 && transform.d() != -1) || transform.b() || transform.c())
                    rectToClipOut.inflate(-1.0f);
            }

            if (!rectToClipOut.isEmpty())
                paintingContext.context.clipOut(rectToClipOut.rect());

            paintingContext.context.fillRect(adjustedFillRect.rect(), Color::black);
        }
    };

    auto paintInsetShadow = [&](const ShadowData& shadow) {
        auto shadowOffset = roundSizeToDevicePixels({ shadow.x().value(), shadow.y().value() }, paintingContext.deviceScaleFactor);
        float shadowPaintingExtent = ceilToDevicePixel(shadow.paintingExtent(), paintingContext.deviceScaleFactor);
        float shadowSpread = roundToDevicePixel(shadow.spread().value(), paintingContext.deviceScaleFactor);
        auto shadowRadius = shadow.radius();

        auto holeRect = borderRect.rect();
        holeRect.inflate(-shadowSpread);

        if (!m_includeLeftEdge) {
            // FIXME: Need to take writing mode into account.
            holeRect.shiftXEdgeBy(-(std::max<float>(shadowOffset.width(), 0) + shadowPaintingExtent + shadowSpread));
        }

        if (!m_includeRightEdge) {
            // FIXME: Need to take writing mode into account.
            holeRect.setWidth(holeRect.width() - std::min<float>(shadowOffset.width(), 0) + shadowPaintingExtent + shadowSpread);
        }

        auto roundedHoleRect = FloatRoundedRect { holeRect, borderRect.radii() };
        if (shadowSpread && roundedHoleRect.isRounded()) {
            auto roundedRectCorrectingForSpread = [&]() {
                bool horizontal = true; // FIXME: Handle writing modes.
                auto borderWidth = borderWidths(boxDecorationData->borderEdges());

                float leftWidth { (!horizontal || m_includeLeftEdge) ? borderWidth.left() + shadowSpread : 0 };
                float rightWidth { (!horizontal || m_includeRightEdge) ? borderWidth.right() + shadowSpread : 0 };
                float topWidth { (horizontal || m_includeLeftEdge) ? borderWidth.top() + shadowSpread : 0 };
                float bottomWidth { (horizontal || m_includeRightEdge) ? borderWidth.bottom() + shadowSpread : 0 };

                return roundedInsetBorderForRect(m_borderRect.rect(), m_borderRect.radii(), { topWidth, rightWidth, bottomWidth, leftWidth }, m_includeLeftEdge, m_includeRightEdge);
            }();
            roundedHoleRect.setRadii(roundedRectCorrectingForSpread.radii());
        }

        if (roundedHoleRect.isEmpty()) {
            if (hasBorderRadius)
                paintingContext.context.fillRoundedRect(borderRect, shadow.color());
            else
                paintingContext.context.fillRect(borderRect.rect(), shadow.color());
            return;
        }

        auto areaCastingShadowInHole = [](const FloatRect& holeRect, float shadowExtent, float shadowSpread, FloatSize shadowOffset) {
            auto bounds(holeRect);
            
            bounds.inflate(shadowExtent);

            if (shadowSpread < 0)
                bounds.inflate(-shadowSpread);
            
            auto offsetBounds = bounds;
            offsetBounds.move(-shadowOffset);
            return unionRect(bounds, offsetBounds);
        };

        Color fillColor = shadow.color().opaqueColor();
        auto shadowCastingRect = areaCastingShadowInHole(borderRect.rect(), shadowPaintingExtent, shadowSpread, shadowOffset);

        GraphicsContextStateSaver stateSaver(paintingContext.context);
        if (hasBorderRadius)
            paintingContext.context.clipRoundedRect(borderRect);
        else
            paintingContext.context.clip(borderRect.rect());

        float xOffset = shadowCastingRect.width() + std::max<float>(0, shadowOffset.width()) + shadowPaintingExtent - 2 * shadowSpread + 1.0f;
        auto extraOffset = FloatSize { std::ceil(xOffset), 0 };

        paintingContext.context.translate(extraOffset);
        shadowOffset -= extraOffset;

        paintingContext.context.setShadow(shadowOffset, shadowRadius.value(), shadow.color(), shadow.isWebkitBoxShadow() ? ShadowRadiusMode::Legacy : ShadowRadiusMode::Default);
        paintingContext.context.fillRectWithRoundedHole(shadowCastingRect, roundedHoleRect, fillColor);
    };


    for (auto* shadow = m_box.style().boxShadow(); shadow; shadow = shadow->next()) {
        if (shadow->style() != shadowStyle)
            continue;

        LayoutSize shadowOffset(shadow->x().value(), shadow->y().value());
        if (shadowOffset.isZero() && shadow->radius().isZero() && shadow->spread().isZero())
            continue;

        if (shadow->style() == ShadowStyle::Normal)
            paintNormalShadow(*shadow);
        else
            paintInsetShadow(*shadow);
    }
}

void BoxDecorationPainter::paintBackgroundImages(PaintingContext& paintingContext) const
{
    const auto& style = m_box.style();

    Vector<const FillLayer*, 8> layers;

    for (auto* layer = style.backgroundLayers(); layer; layer = layer->next())
        layers.append(layer);

    auto* boxDecorationData = m_box.boxDecorationData();
    ASSERT(boxDecorationData);

    auto& layerGeometryList = boxDecorationData->backgroundImageGeometry();

    for (int i = layers.size() - 1; i >=0; --i) {
        const auto* layer = layers[i];
        const auto& geometry = layerGeometryList[i];
        paintFillLayer(paintingContext, *layer, geometry);
    }
}

FloatRoundedRect BoxDecorationPainter::innerBorderRoundedRect() const
{
    return m_box.innerBorderRoundedRect();
}

FloatRoundedRect BoxDecorationPainter::backgroundRoundedRectAdjustedForBleedAvoidance(const PaintingContext& paintingContext) const
{
    if (m_bleedAvoidance == BackgroundBleedAvoidance::ShrinkBackground) {
        // We shrink the rectangle by one device pixel on each side because the bleed is one pixel maximum.
        return roundedRectWithIncludedRadii(shrinkRectByOneDevicePixel(paintingContext, m_borderRect.rect()), m_borderRect.radii(), m_includeLeftEdge, m_includeRightEdge);
    }

    if (m_bleedAvoidance == BackgroundBleedAvoidance::BackgroundOverBorder)
        return innerBorderRoundedRect();

    return roundedRectWithIncludedRadii(m_borderRect.rect(), m_borderRect.radii(), m_includeLeftEdge, m_includeRightEdge);
}

void BoxDecorationPainter::paintBackground(PaintingContext& paintingContext) const
{
    auto borderBoxRect = m_box.absoluteBorderBoxRect();
    const auto& style = m_box.style();

    if (style.hasBackground()) {
        GraphicsContextStateSaver stateSaver(paintingContext.context, false);
        if (m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer && m_box.hasBorderRadius()) {
            stateSaver.save();
            auto outerBorder = backgroundRoundedRectAdjustedForBleedAvoidance(paintingContext);
            paintingContext.context.clipRoundedRect(outerBorder);
        }
    
        paintingContext.context.fillRect(borderBoxRect, style.backgroundColor());

        if (style.hasBackgroundImage())
            paintBackgroundImages(paintingContext);
    }
}

BackgroundBleedAvoidance BoxDecorationPainter::determineBackgroundBleedAvoidance(const BoxModelBox& box, PaintingContext& paintingContext)
{
    if (paintingContext.context.paintingDisabled())
        return BackgroundBleedAvoidance::None;

    auto& style = box.style();
    auto* boxDecorationData = box.boxDecorationData();
    
    auto hasBackgroundAndRoundedBorder = [&] {
        if (!boxDecorationData)
            return false;

        // FIXME: Consult border-image.
        return style.hasBackground() && boxDecorationData->hasBorder() && box.hasBorderRadius();
    };

    if (!hasBackgroundAndRoundedBorder())
        return BackgroundBleedAvoidance::None;

    auto ctm = paintingContext.context.getCTM();
    auto contextScaling = FloatSize { static_cast<float>(ctm.xScale()), static_cast<float>(ctm.yScale()) };
    if (boxDecorationData->borderObscuresBackgroundEdge(contextScaling))
        return BackgroundBleedAvoidance::ShrinkBackground;

    if (boxDecorationData->borderObscuresBackground() && style.backgroundHasOpaqueTopLayer())
        return BackgroundBleedAvoidance::BackgroundOverBorder;

    return BackgroundBleedAvoidance::UseTransparencyLayer;
}

void BoxDecorationPainter::paintBackgroundAndBorders(PaintingContext& paintingContext) const
{
    // FIXME: Table decoration painting is special.
    
    switch (m_bleedAvoidance) {
    case BackgroundBleedAvoidance::BackgroundOverBorder:
        paintBoxShadow(paintingContext, ShadowStyle::Normal);
        paintBorders(paintingContext);
        paintBackground(paintingContext);
        paintBoxShadow(paintingContext, ShadowStyle::Inset);
        break;

    case BackgroundBleedAvoidance::UseTransparencyLayer: {
        paintBoxShadow(paintingContext, ShadowStyle::Normal);
        GraphicsContextStateSaver stateSaver(paintingContext.context);
        auto outerBorder = borderRoundedRect();
        paintingContext.context.clipRoundedRect(outerBorder);
        paintingContext.context.beginTransparencyLayer(1);

        paintBackground(paintingContext);
        paintBoxShadow(paintingContext, ShadowStyle::Inset);
        paintBorders(paintingContext);

        paintingContext.context.endTransparencyLayer();
        break;
    }
    
    case BackgroundBleedAvoidance::ShrinkBackground:
    case BackgroundBleedAvoidance::None:
        paintBoxShadow(paintingContext, ShadowStyle::Normal);
        paintBackground(paintingContext);
        paintBoxShadow(paintingContext, ShadowStyle::Inset);
        paintBorders(paintingContext);
        break;
    }
}

} // namespace Display
} // namespace WebCore

#endif // ENABLE(LAYOUT_FORMATTING_CONTEXT)
