/*
 * Copyright (C) 2019 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 "InlineLine.h"

#if ENABLE(LAYOUT_FORMATTING_CONTEXT)

#include "FontCascade.h"
#include "InlineFormattingContext.h"
#include "InlineSoftLineBreakItem.h"
#include "LayoutBoxGeometry.h"
#include "TextFlags.h"
#include "TextUtil.h"
#include <wtf/IsoMallocInlines.h>

namespace WebCore {
namespace Layout {

Line::Line(const InlineFormattingContext& inlineFormattingContext)
    : m_inlineFormattingContext(inlineFormattingContext)
    , m_trimmableTrailingContent(m_runs)
{
}

Line::~Line()
{
}

void Line::initialize(const Vector<InlineItem>& lineSpanningInlineBoxes, bool collapseLeadingNonBreakingSpace)
{
    m_collapseLeadingNonBreakingSpace = collapseLeadingNonBreakingSpace;
    m_inlineBoxListWithClonedDecorationEnd.clear();
    m_clonedEndDecorationWidthForInlineBoxRuns = { };
    m_nonSpanningInlineLevelBoxCount = 0;
    m_hasNonDefaultBidiLevelRun = false;
    m_contentLogicalWidth = { };
    m_runs.clear();
    resetTrailingContent();
    auto appendLineSpanningInlineBoxes = [&] {
        for (auto& inlineBoxStartItem : lineSpanningInlineBoxes) {
#if ENABLE(CSS_BOX_DECORATION_BREAK)
            if (inlineBoxStartItem.style().boxDecorationBreak() != BoxDecorationBreak::Clone)
                m_runs.append({ inlineBoxStartItem, lastRunLogicalRight(), { } });
            else {
                // https://drafts.csswg.org/css-break/#break-decoration
                // clone: Each box fragment is independently wrapped with the border, padding, and margin.
                auto& inlineBoxGeometry = formattingContext().geometryForBox(inlineBoxStartItem.layoutBox());
                auto marginBorderAndPaddingStart = inlineBoxGeometry.marginStart() + inlineBoxGeometry.borderStart() + inlineBoxGeometry.paddingStart().value_or(0_lu);
                auto runLogicalLeft = lastRunLogicalRight();
                m_runs.append({ inlineBoxStartItem, runLogicalLeft, marginBorderAndPaddingStart });
                // Do not let negative margin make the content shorter than it already is.
                m_contentLogicalWidth = std::max(m_contentLogicalWidth, runLogicalLeft + marginBorderAndPaddingStart);
                m_contentLogicalWidth += addBorderAndPaddingEndForInlineBoxDecorationClone(inlineBoxStartItem);
            }
#else
            m_runs.append({ inlineBoxStartItem, lastRunLogicalRight(), { } });
#endif
        }
    };
    appendLineSpanningInlineBoxes();
}

void Line::resetTrailingContent()
{
    m_trimmableTrailingContent.reset();
    m_hangingTrailingContent.reset();
    m_trailingSoftHyphenWidth = { };
}

void Line::applyRunExpansion(InlineLayoutUnit horizontalAvailableSpace)
{
    ASSERT(formattingContext().root().style().textAlign() == TextAlignMode::Justify || formattingContext().root().style().textAlignLast() == TextAlignLast::Justify);
    // Text is justified according to the method specified by the text-justify property,
    // in order to exactly fill the line box. Unless otherwise specified by text-align-last,
    // the last line before a forced break or the end of the block is start-aligned.
    if (m_runs.isEmpty() || m_runs.last().isLineBreak())
        return;
    // A hanging glyph is still enclosed inside its parent inline box and still participates in text justification:
    // its character advance is just not measured when determining how much content fits on the line, how much the line’s contents
    // need to be expanded or compressed for justification, or how to position the content within the line box for text alignment.
    auto spaceToDistribute = horizontalAvailableSpace - contentLogicalWidth() + m_hangingTrailingContent.width();
    if (spaceToDistribute <= 0)
        return;
    // Collect and distribute the expansion opportunities.
    size_t lineExpansionOpportunities = 0;
    Vector<size_t> runsExpansionOpportunities(m_runs.size());
    Vector<ExpansionBehavior> runsExpansionBehaviors(m_runs.size());
    auto lastRunIndexWithContent = std::optional<size_t> { };

    // Line start behaves as if we had an expansion here (i.e. fist runs should not start with allowing left expansion).
    auto runIsAfterExpansion = true;
    auto hangingTrailingContentLength = m_hangingTrailingContent.length();
    for (size_t runIndex = 0; runIndex < m_runs.size(); ++runIndex) {
        auto& run = m_runs[runIndex];
        auto expansionBehavior = ExpansionBehavior::defaultBehavior();
        size_t expansionOpportunitiesInRun = 0;

        // FIXME: Check why we don't apply expansion when whitespace is preserved.
        if (run.isText() && (!TextUtil::shouldPreserveSpacesAndTabs(run.layoutBox()) || hangingTrailingContentLength)) {
            if (run.hasTextCombine())
                expansionBehavior = ExpansionBehavior::forbidAll();
            else {
                expansionBehavior.left = runIsAfterExpansion ? ExpansionBehavior::Behavior::Forbid : ExpansionBehavior::Behavior::Allow;
                expansionBehavior.right = ExpansionBehavior::Behavior::Allow;
                auto& textContent = *run.textContent();
                // Trailing hanging whitespace sequence is ignored when computing the expansion opportunities.
                auto hangingTrailingContentInCurrentRun = std::min(textContent.length, hangingTrailingContentLength);
                auto length = textContent.length - hangingTrailingContentInCurrentRun;
                hangingTrailingContentLength -= hangingTrailingContentInCurrentRun;
                std::tie(expansionOpportunitiesInRun, runIsAfterExpansion) = FontCascade::expansionOpportunityCount(StringView(downcast<InlineTextBox>(run.layoutBox()).content()).substring(textContent.start, length), run.inlineDirection(), expansionBehavior);
            }
        } else if (run.isBox())
            runIsAfterExpansion = false;

        runsExpansionBehaviors[runIndex] = expansionBehavior;
        runsExpansionOpportunities[runIndex] = expansionOpportunitiesInRun;
        lineExpansionOpportunities += expansionOpportunitiesInRun;

        if (run.isText() || run.isBox())
            lastRunIndexWithContent = runIndex;
    }
    // Forbid right expansion in the last run to prevent trailing expansion at the end of the line.
    if (lastRunIndexWithContent && runsExpansionOpportunities[*lastRunIndexWithContent]) {
        runsExpansionBehaviors[*lastRunIndexWithContent].right = ExpansionBehavior::Behavior::Forbid;
        if (runIsAfterExpansion) {
            // When the last run has an after expansion (e.g. CJK ideograph) we need to remove this trailing expansion opportunity.
            // Note that this is not about trailing collapsible whitespace as at this point we trimmed them all.
            ASSERT(lineExpansionOpportunities && runsExpansionOpportunities[*lastRunIndexWithContent]);
            --lineExpansionOpportunities;
            --runsExpansionOpportunities[*lastRunIndexWithContent];
        }
    }
    // Anything to distribute?
    if (!lineExpansionOpportunities)
        return;
    // Distribute the extra space.
    auto expansionToDistribute = spaceToDistribute / lineExpansionOpportunities;
    auto accumulatedExpansion = InlineLayoutUnit { };
    for (size_t runIndex = 0; runIndex < m_runs.size(); ++runIndex) {
        auto& run = m_runs[runIndex];
        // Expand and move runs by the accumulated expansion.
        run.moveHorizontally(accumulatedExpansion);
        auto computedExpansion = expansionToDistribute * runsExpansionOpportunities[runIndex];
        run.setExpansion({ runsExpansionBehaviors[runIndex], computedExpansion });
        run.shrinkHorizontally(-computedExpansion);
        accumulatedExpansion += computedExpansion;
    }
    // Content grows as runs expand.
    m_contentLogicalWidth += accumulatedExpansion;
}

void Line::removeTrailingTrimmableContent(ShouldApplyTrailingWhiteSpaceFollowedByBRQuirk shouldApplyTrailingWhiteSpaceFollowedByBRQuirk)
{
    if (m_trimmableTrailingContent.isEmpty() || m_runs.isEmpty())
        return;

    if (shouldApplyTrailingWhiteSpaceFollowedByBRQuirk == ShouldApplyTrailingWhiteSpaceFollowedByBRQuirk::Yes) {
        auto isTextAlignRight = [&] {
            auto textAlign = formattingContext().root().style().textAlign();
            return textAlign == TextAlignMode::Right
                || textAlign == TextAlignMode::WebKitRight
                || textAlign == TextAlignMode::End;
            }();

        if (m_runs.last().isLineBreak() && !isTextAlignRight) {
            m_trimmableTrailingContent.reset();
            return;
        }
    }
    m_contentLogicalWidth -= m_trimmableTrailingContent.remove();
}

void Line::removeHangingGlyphs()
{
    ASSERT(m_trimmableTrailingContent.isEmpty());
    m_contentLogicalWidth -= m_hangingTrailingContent.width();
    m_hangingTrailingContent.reset();
}

void Line::resetBidiLevelForTrailingWhitespace(UBiDiLevel rootBidiLevel)
{
    ASSERT(m_trimmableTrailingContent.isEmpty());
    if (!m_hasNonDefaultBidiLevelRun)
        return;
    // UAX#9 L1: trailing whitespace should use paragraph direction.
    // see https://unicode.org/reports/tr9/#L1
    for (auto index = m_runs.size(); index--;) {
        auto& run = m_runs[index];
        if (run.isBox() || run.isLineBreak() || (run.isText() && !run.hasTrailingWhitespace()))
            break;

        if (!run.hasTrailingWhitespace()) {
            // Skip non-content type of runs e.g. <span>
            continue;
        }

        if (run.isWhitespaceOnly())
            run.setBidiLevel(rootBidiLevel);
        else {
            auto detachedTrailingRun = *run.detachTrailingWhitespace();
            detachedTrailingRun.setBidiLevel(rootBidiLevel);
            if (index == m_runs.size() - 1)
                m_runs.append(detachedTrailingRun);
            else
                m_runs.insert(index + 1, detachedTrailingRun);
            // There can't be any trailing whitespace in front of this partially whitespace content.
            break;
        }
    }
}

void Line::append(const InlineItem& inlineItem, const RenderStyle& style, InlineLayoutUnit logicalWidth)
{
    if (inlineItem.isText())
        appendTextContent(downcast<InlineTextItem>(inlineItem), style, logicalWidth);
    else if (inlineItem.isLineBreak())
        appendLineBreak(inlineItem, style);
    else if (inlineItem.isWordBreakOpportunity())
        appendWordBreakOpportunity(inlineItem, style);
    else if (inlineItem.isInlineBoxStart())
        appendInlineBoxStart(inlineItem, style, logicalWidth);
    else if (inlineItem.isInlineBoxEnd())
        appendInlineBoxEnd(inlineItem, style, logicalWidth);
    else if (inlineItem.layoutBox().isReplacedBox())
        appendReplacedInlineLevelBox(inlineItem, style, logicalWidth);
    else if (inlineItem.isBox())
        appendNonReplacedInlineLevelBox(inlineItem, style, logicalWidth);
    else
        ASSERT_NOT_REACHED();
    m_hasNonDefaultBidiLevelRun = m_hasNonDefaultBidiLevelRun || inlineItem.bidiLevel() != UBIDI_DEFAULT_LTR;
}

void Line::appendInlineBoxStart(const InlineItem& inlineItem, const RenderStyle& style, InlineLayoutUnit logicalWidth)
{
    // This is really just a placeholder to mark the start of the inline box <span>.
    ++m_nonSpanningInlineLevelBoxCount;
    auto logicalLeft = lastRunLogicalRight();
    // Incoming logical width includes the cloned decoration end to be able to do line breaking.
    auto borderAndPaddingEndForDecorationClone = addBorderAndPaddingEndForInlineBoxDecorationClone(inlineItem);
    // Do not let negative margin make the content shorter than it already is.
    m_contentLogicalWidth = std::max(m_contentLogicalWidth, logicalLeft + logicalWidth);

    auto marginStart = formattingContext().geometryForBox(inlineItem.layoutBox()).marginStart();
    if (marginStart >= 0) {
        m_runs.append({ inlineItem, style, logicalLeft, logicalWidth - borderAndPaddingEndForDecorationClone });
        return;
    }
    // Negative margin-start pulls the content to the logical left direction.
    m_runs.append({ inlineItem, style, logicalLeft + marginStart, logicalWidth - marginStart - borderAndPaddingEndForDecorationClone });
}

void Line::appendInlineBoxEnd(const InlineItem& inlineItem, const RenderStyle& style, InlineLayoutUnit logicalWidth)
{
    // This is really just a placeholder to mark the end of the inline box </span>.
    auto removeTrailingLetterSpacing = [&] {
        if (!m_trimmableTrailingContent.isTrailingRunPartiallyTrimmable())
            return;
        m_contentLogicalWidth -= m_trimmableTrailingContent.removePartiallyTrimmableContent();
    };
    // Prevent trailing letter-spacing from spilling out of the inline box.
    // https://drafts.csswg.org/css-text-3/#letter-spacing-property See example 21.
    removeTrailingLetterSpacing();
    m_contentLogicalWidth -= removeBorderAndPaddingEndForInlineBoxDecorationClone(inlineItem);
    auto logicalLeft = lastRunLogicalRight();
    m_runs.append({ inlineItem, style, logicalLeft, logicalWidth });
    // Do not let negative margin make the content shorter than it already is.
    m_contentLogicalWidth = std::max(m_contentLogicalWidth, logicalLeft + logicalWidth);
}

void Line::appendTextContent(const InlineTextItem& inlineTextItem, const RenderStyle& style, InlineLayoutUnit logicalWidth)
{
    auto willCollapseCompletely = [&] {
        if (!inlineTextItem.isWhitespace()) {
            auto isLeadingCollapsibleNonBreakingSpace = [&] {
                // Let's check for leading non-breaking space collapsing to match legacy line layout quirk.
                if (!inlineTextItem.isCollapsibleNonBreakingSpace() || !m_collapseLeadingNonBreakingSpace)
                    return false;
                for (auto& run : makeReversedRange(m_runs)) {
                    if (run.isBox() || run.isText())
                        return false;
                }
                return true;
            };
            return isLeadingCollapsibleNonBreakingSpace();
        }
        if (InlineTextItem::shouldPreserveSpacesAndTabs(inlineTextItem))
            return false;
        // This content is collapsible. Let's check if the last item is collapsed.
        for (auto& run : makeReversedRange(m_runs)) {
            if (run.isBox())
                return false;
            // https://drafts.csswg.org/css-text-3/#white-space-phase-1
            // Any collapsible space immediately following another collapsible space—even one outside the boundary of the inline containing that space,
            // provided both spaces are within the same inline formatting context—is collapsed to have zero advance width.
            if (run.isText())
                return run.hasCollapsibleTrailingWhitespace();
            ASSERT(run.isListMarker() || run.isLineSpanningInlineBoxStart() || run.isInlineBoxStart() || run.isInlineBoxEnd() || run.isWordBreakOpportunity());
        }
        // Leading whitespace.
        return true;
    };

    if (willCollapseCompletely())
        return;

    auto needsNewRun = [&] {
        if (m_runs.isEmpty())
            return true;
        auto& lastRun = m_runs.last();
        if (&lastRun.layoutBox() != &inlineTextItem.layoutBox())
            return true;
        if (lastRun.bidiLevel() != inlineTextItem.bidiLevel())
            return true;
        if (!lastRun.isText())
            return true;
        if (lastRun.hasCollapsedTrailingWhitespace())
            return true;
        if (style.fontCascade().wordSpacing()
            && (inlineTextItem.isWordSeparator() || (lastRun.isWordSeparator() && lastRun.bidiLevel() != UBIDI_DEFAULT_LTR)))
            return true;
        if (inlineTextItem.isZeroWidthSpaceSeparator())
            return true;
        return false;
    }();
    auto oldContentLogicalWidth = contentLogicalWidth();
    if (needsNewRun) {
        // Note, negative word spacing may cause glyph overlap.
        auto runLogicalLeft = lastRunLogicalRight() + (inlineTextItem.isWordSeparator() ? style.fontCascade().wordSpacing() : 0.0f);
        m_runs.append({ inlineTextItem, style, runLogicalLeft, logicalWidth });
        // Note that the _content_ logical right may be larger than the _run_ logical right.
        auto contentLogicalRight = runLogicalLeft + logicalWidth + m_clonedEndDecorationWidthForInlineBoxRuns;
        m_contentLogicalWidth = std::max(oldContentLogicalWidth, contentLogicalRight);
    } else if (style.letterSpacing() >= 0) {
        auto& lastRun = m_runs.last();
        lastRun.expand(inlineTextItem, logicalWidth);
        // Ensure that property values that act like negative margin are not making the line wider.
        m_contentLogicalWidth = std::max(oldContentLogicalWidth, lastRun.logicalRight());
    } else {
        auto& lastRun = m_runs.last();
        ASSERT(lastRun.isText());
        // Negative letter spacing should only shorten the content to the boundary of the previous run.
        // FIXME: We may need to traverse all the way to the previous non-text run (or even across inline boxes).
        auto contentWidthWithoutLastTextRun = [&] {
            if (style.fontCascade().wordSpacing() >= 0)
                return m_contentLogicalWidth - std::max(0.f, lastRun.logicalWidth());
            // FIXME: Let's see if we need to optimize for this is the rare case of both letter and word spacing being negative.
            auto rightMostPosition = InlineLayoutUnit { };
            for (auto& run : makeReversedRange(m_runs))
                rightMostPosition = std::max(rightMostPosition, run.logicalRight());
            return std::max(0.f, rightMostPosition);
        }();
        auto lastRunLogicalRight = lastRun.logicalRight();
        lastRun.expand(inlineTextItem, logicalWidth);
        m_contentLogicalWidth = std::max(contentWidthWithoutLastTextRun, lastRunLogicalRight + logicalWidth);
    }

    // Handle trailing content, specifically whitespace and letter spacing.
    auto lastRunIndex = m_runs.size() - 1;
    if (inlineTextItem.isWhitespace()) {
        if (InlineTextItem::shouldPreserveSpacesAndTabs(inlineTextItem)) {
            m_trimmableTrailingContent.reset();
            if (m_runs[lastRunIndex].shouldTrailingWhitespaceHang())
                m_hangingTrailingContent.add(inlineTextItem, logicalWidth);
        } else  {
            m_hangingTrailingContent.reset();
            auto trimmableWidth = logicalWidth;
            auto trimmableContentOffset = (contentLogicalWidth() - oldContentLogicalWidth) - trimmableWidth;
            m_trimmableTrailingContent.addFullyTrimmableContent(lastRunIndex, trimmableContentOffset, trimmableWidth);
        }
        m_trailingSoftHyphenWidth = { };
    } else {
        resetTrailingContent();
        if (style.letterSpacing() > 0 && !formattingContext().layoutState().shouldIgnoreTrailingLetterSpacing())
            m_trimmableTrailingContent.addPartiallyTrimmableContent(lastRunIndex, style.letterSpacing());
        if (inlineTextItem.hasTrailingSoftHyphen())
            m_trailingSoftHyphenWidth = style.fontCascade().width(TextRun { StringView { style.hyphenString() } });
    }
}

void Line::appendNonReplacedInlineLevelBox(const InlineItem& inlineItem, const RenderStyle& style, InlineLayoutUnit marginBoxLogicalWidth)
{
    resetTrailingContent();
    // Do not let negative margin make the content shorter than it already is.
    m_contentLogicalWidth = std::max(m_contentLogicalWidth, lastRunLogicalRight() + marginBoxLogicalWidth);
    ++m_nonSpanningInlineLevelBoxCount;
    auto marginStart = formattingContext().geometryForBox(inlineItem.layoutBox()).marginStart();
    if (marginStart >= 0) {
        m_runs.append({ inlineItem, style, lastRunLogicalRight(), marginBoxLogicalWidth });
        return;
    }
    // Negative margin-start pulls the content to the logical left direction.
    // Negative margin also squeezes the margin box, we need to stretch it to make sure the subsequent content won't overlap.
    // e.g. <img style="width: 100px; margin-left: -100px;"> pulls the replaced box to -100px with the margin box width of 0px.
    // Instead we need to position it at -100px and size it to 100px so the subsequent content starts at 0px. 
    m_runs.append({ inlineItem, style, lastRunLogicalRight() + marginStart, marginBoxLogicalWidth - marginStart });
}

void Line::appendReplacedInlineLevelBox(const InlineItem& inlineItem, const RenderStyle& style, InlineLayoutUnit marginBoxLogicalWidth)
{
    ASSERT(inlineItem.layoutBox().isReplacedBox());
    // FIXME: Surely replaced boxes behave differently.
    appendNonReplacedInlineLevelBox(inlineItem, style, marginBoxLogicalWidth);
}

void Line::appendLineBreak(const InlineItem& inlineItem, const RenderStyle& style)
{
    m_trailingSoftHyphenWidth = { };
    if (inlineItem.isHardLineBreak()) {
        ++m_nonSpanningInlineLevelBoxCount;
        return m_runs.append({ inlineItem, style, lastRunLogicalRight() });
    }
    // Soft line breaks (preserved new line characters) require inline text boxes for compatibility reasons.
    ASSERT(inlineItem.isSoftLineBreak());
    m_runs.append({ downcast<InlineSoftLineBreakItem>(inlineItem), inlineItem.style(), lastRunLogicalRight() });
}

void Line::appendWordBreakOpportunity(const InlineItem& inlineItem, const RenderStyle& style)
{
    m_runs.append({ inlineItem, style, lastRunLogicalRight() });
}

InlineLayoutUnit Line::addBorderAndPaddingEndForInlineBoxDecorationClone(const InlineItem& inlineBoxStartItem)
{
    ASSERT(inlineBoxStartItem.isInlineBoxStart());
#if ENABLE(CSS_BOX_DECORATION_BREAK)
    if (inlineBoxStartItem.style().boxDecorationBreak() != BoxDecorationBreak::Clone)
        return { };
    // https://drafts.csswg.org/css-break/#break-decoration
    auto& inlineBoxGeometry = formattingContext().geometryForBox(inlineBoxStartItem.layoutBox());
    auto borderAndPaddingEnd = inlineBoxGeometry.borderEnd() + inlineBoxGeometry.paddingEnd().value_or(0_lu);
    m_inlineBoxListWithClonedDecorationEnd.add(&inlineBoxStartItem.layoutBox(), borderAndPaddingEnd);
    m_clonedEndDecorationWidthForInlineBoxRuns += borderAndPaddingEnd;
    return borderAndPaddingEnd;
#endif
    return { };
}

InlineLayoutUnit Line::removeBorderAndPaddingEndForInlineBoxDecorationClone(const InlineItem& inlineBoxEndItem)
{
    ASSERT(inlineBoxEndItem.isInlineBoxEnd());
#if ENABLE(CSS_BOX_DECORATION_BREAK)
    auto borderAndPaddingEnd = m_inlineBoxListWithClonedDecorationEnd.take(&inlineBoxEndItem.layoutBox());
    if (std::isinf(borderAndPaddingEnd))
        return { };
    // This inline box end now contributes to the line content width in the regular way, so let's remove
    // it from the side structure where we keep track of the "not-yet placed but space taking" decorations.
    m_clonedEndDecorationWidthForInlineBoxRuns -= borderAndPaddingEnd;
    return borderAndPaddingEnd;
#endif
    return { };
}

void Line::addTrailingHyphen(InlineLayoutUnit hyphenLogicalWidth)
{
    for (auto& run : makeReversedRange(m_runs)) {
        if (!run.isText())
            continue;
        run.setNeedsHyphen(hyphenLogicalWidth);
        m_contentLogicalWidth += hyphenLogicalWidth;
        return;
    }
    ASSERT_NOT_REACHED();
}

const InlineFormattingContext& Line::formattingContext() const
{
    return m_inlineFormattingContext;
}

Line::TrimmableTrailingContent::TrimmableTrailingContent(RunList& runs)
    : m_runs(runs)
{
}

void Line::TrimmableTrailingContent::addFullyTrimmableContent(size_t runIndex, InlineLayoutUnit trimmableContentOffset, InlineLayoutUnit trimmableWidth)
{
    // Any subsequent trimmable whitespace should collapse to zero advanced width and ignored at ::appendTextContent().
    ASSERT(!m_hasFullyTrimmableContent);
    m_fullyTrimmableWidth = trimmableContentOffset + trimmableWidth;
    m_trimmableContentOffset = trimmableContentOffset;
    // Note that just because the trimmable width is 0 (font-size: 0px), it does not mean we don't have a trimmable trailing content.
    m_hasFullyTrimmableContent = true;
    m_firstTrimmableRunIndex = m_firstTrimmableRunIndex.value_or(runIndex);
}

void Line::TrimmableTrailingContent::addPartiallyTrimmableContent(size_t runIndex, InlineLayoutUnit trimmableWidth)
{
    // Do not add trimmable letter spacing after a fully trimmable whitespace.
    ASSERT(!m_firstTrimmableRunIndex);
    ASSERT(!m_hasFullyTrimmableContent);
    ASSERT(!m_partiallyTrimmableWidth);
    ASSERT(trimmableWidth);
    m_partiallyTrimmableWidth = trimmableWidth;
    m_firstTrimmableRunIndex = runIndex;
}

InlineLayoutUnit Line::TrimmableTrailingContent::remove()
{
    // Remove trimmable trailing content and move all the subsequent trailing runs.
    // <span> </span><span></span>
    // [trailing whitespace][inline box end][inline box start][inline box end]
    // Trim the whitespace run and move the trailing inline box runs to the logical left.
    ASSERT(!isEmpty());
    auto& trimmableRun = m_runs[*m_firstTrimmableRunIndex];
    ASSERT(trimmableRun.isText());

    auto trimmedWidth = m_trimmableContentOffset;
    if (m_hasFullyTrimmableContent)
        trimmedWidth += trimmableRun.removeTrailingWhitespace();
    if (m_partiallyTrimmableWidth)
        trimmedWidth += trimmableRun.removeTrailingLetterSpacing();

    // When the trimmable run is followed by some non-content runs, we need to adjust their horizontal positions.
    // e.g. <div>text is followed by trimmable content    <span> </span></div>
    // When the [text...] run is trimmed (trailing whitespace is removed), both "<span>" and "</span>" runs
    // need to be moved horizontally to catch up with the [text...] run. Note that the whitespace inside the <span> does
    // not produce a run since in ::appendText() we see it as a fully collapsible run.
    for (auto index = *m_firstTrimmableRunIndex + 1; index < m_runs.size(); ++index) {
        auto& run = m_runs[index];
        ASSERT(run.isWordBreakOpportunity() || run.isLineSpanningInlineBoxStart() || run.isInlineBoxStart() || run.isInlineBoxEnd() || run.isLineBreak());
        run.moveHorizontally(-trimmedWidth);
    }
    if (!trimmableRun.textContent()->length) {
        // This trimmable run is fully collapsed now (e.g. <div><img>    <span></span></div>).
        // We don't need to keep it around anymore.
        m_runs.remove(*m_firstTrimmableRunIndex);
    }
    reset();
    return trimmedWidth;
}

InlineLayoutUnit Line::TrimmableTrailingContent::removePartiallyTrimmableContent()
{
    // Partially trimmable content is always gated by a fully trimmable content.
    // We can't just trim spacing in the middle.
    ASSERT(!m_fullyTrimmableWidth);
    return remove();
}

void Line::HangingTrailingContent::add(const InlineTextItem& trailingWhitespace, InlineLayoutUnit logicalWidth)
{
    // When a glyph at the start or end edge of a line hangs, it is not considered when measuring the line’s contents for fit, alignment, or justification.
    // Depending on the line’s alignment/justification, this can result in the mark being placed outside the line box.
    // https://drafts.csswg.org/css-text-3/#hanging
    ASSERT(trailingWhitespace.isWhitespace());
    m_width += logicalWidth;
    m_length += trailingWhitespace.length();
}

inline static Line::Run::Type toLineRunType(const InlineItem& inlineItem)
{
    switch (inlineItem.type()) {
    case InlineItem::Type::HardLineBreak:
        return Line::Run::Type::HardLineBreak;
    case InlineItem::Type::WordBreakOpportunity:
        return Line::Run::Type::WordBreakOpportunity;
    case InlineItem::Type::Box:
        return inlineItem.layoutBox().isListMarker() ? Line::Run::Type::ListMarker : Line::Run::Type::GenericInlineLevelBox;
    case InlineItem::Type::InlineBoxStart:
        return Line::Run::Type::InlineBoxStart;
    case InlineItem::Type::InlineBoxEnd:
        return Line::Run::Type::InlineBoxEnd;
    default:
        RELEASE_ASSERT_NOT_REACHED();
    }
    return { };
}

std::optional<Line::Run::TrailingWhitespace::Type> Line::Run::trailingWhitespaceType(const InlineTextItem& inlineTextItem)
{
    if (!inlineTextItem.isWhitespace())
        return { };
    if (InlineTextItem::shouldPreserveSpacesAndTabs(inlineTextItem))
        return { TrailingWhitespace::Type::NotCollapsible };
    if (inlineTextItem.length() == 1)
        return { TrailingWhitespace::Type::Collapsible };
    return { TrailingWhitespace::Type::Collapsed };
}

Line::Run::Run(const InlineItem& inlineItem, const RenderStyle& style, InlineLayoutUnit logicalLeft, InlineLayoutUnit logicalWidth)
    : m_type(toLineRunType(inlineItem))
    , m_layoutBox(&inlineItem.layoutBox())
    , m_style(style)
    , m_logicalLeft(logicalLeft)
    , m_logicalWidth(logicalWidth)
    , m_bidiLevel(inlineItem.bidiLevel())
{
}

Line::Run::Run(const InlineItem& zeroWidhtInlineItem, const RenderStyle& style, InlineLayoutUnit logicalLeft)
    : m_type(toLineRunType(zeroWidhtInlineItem))
    , m_layoutBox(&zeroWidhtInlineItem.layoutBox())
    , m_style(style)
    , m_logicalLeft(logicalLeft)
    , m_bidiLevel(zeroWidhtInlineItem.bidiLevel())
{
}

Line::Run::Run(const InlineItem& lineSpanningInlineBoxItem, InlineLayoutUnit logicalLeft, InlineLayoutUnit logicalWidth)
    : m_type(Type::LineSpanningInlineBoxStart)
    , m_layoutBox(&lineSpanningInlineBoxItem.layoutBox())
    , m_style(lineSpanningInlineBoxItem.style())
    , m_logicalLeft(logicalLeft)
    , m_logicalWidth(logicalWidth)
    , m_bidiLevel(lineSpanningInlineBoxItem.bidiLevel())
{
    ASSERT(lineSpanningInlineBoxItem.isInlineBoxStart());
}

Line::Run::Run(const InlineSoftLineBreakItem& softLineBreakItem, const RenderStyle& style, InlineLayoutUnit logicalLeft)
    : m_type(Type::SoftLineBreak)
    , m_layoutBox(&softLineBreakItem.layoutBox())
    , m_style(style)
    , m_logicalLeft(logicalLeft)
    , m_bidiLevel(softLineBreakItem.bidiLevel())
    , m_textContent({ softLineBreakItem.position(), 1 })
{
}

Line::Run::Run(const InlineTextItem& inlineTextItem, const RenderStyle& style, InlineLayoutUnit logicalLeft, InlineLayoutUnit logicalWidth)
    : m_type(inlineTextItem.isWordSeparator() ? Type::WordSeparator : Type::Text)
    , m_layoutBox(&inlineTextItem.layoutBox())
    , m_style(style)
    , m_logicalLeft(logicalLeft)
    , m_logicalWidth(logicalWidth)
    , m_bidiLevel(inlineTextItem.bidiLevel())
{
    auto length = inlineTextItem.length();
    auto whitespaceType = trailingWhitespaceType(inlineTextItem);
    if (whitespaceType) {
        if (*whitespaceType == TrailingWhitespace::Type::Collapsed)
            length =  1;
        m_trailingWhitespace = { *whitespaceType, logicalWidth, length };
    }
    m_textContent = { inlineTextItem.start(), length };
}

void Line::Run::expand(const InlineTextItem& inlineTextItem, InlineLayoutUnit logicalWidth)
{
    ASSERT(!hasCollapsedTrailingWhitespace());
    ASSERT(isText() && inlineTextItem.isText());
    ASSERT(m_layoutBox == &inlineTextItem.layoutBox());
    ASSERT(m_bidiLevel == inlineTextItem.bidiLevel());

    m_logicalWidth += logicalWidth;
    auto whitespaceType = trailingWhitespaceType(inlineTextItem);

    if (!whitespaceType) {
        m_trailingWhitespace = { };
        m_textContent->length += inlineTextItem.length();
        m_lastNonWhitespaceContentStart = inlineTextItem.start();
        return;
    }
    auto whitespaceWidth = !m_trailingWhitespace ? logicalWidth : m_trailingWhitespace->width + logicalWidth;
    auto trailingWhitespaceLength = *whitespaceType == TrailingWhitespace::Type::Collapsed ? 1 : inlineTextItem.length(); 
    m_trailingWhitespace = { *whitespaceType, whitespaceWidth, trailingWhitespaceLength };
    m_textContent->length += trailingWhitespaceLength;
}

std::optional<Line::Run> Line::Run::detachTrailingWhitespace()
{
    if (!m_trailingWhitespace || isWhitespaceOnly())
        return { };

    ASSERT(m_trailingWhitespace->length < m_textContent->length);
    auto trailingWhitespaceRun = *this;

    auto leadingNonWhitespaceContentLength = m_textContent->length - m_trailingWhitespace->length;
    trailingWhitespaceRun.m_textContent = { m_textContent->start + leadingNonWhitespaceContentLength, m_trailingWhitespace->length, false };

    trailingWhitespaceRun.m_logicalWidth = m_trailingWhitespace->width;
    trailingWhitespaceRun.m_logicalLeft = logicalRight() - m_trailingWhitespace->width;

    trailingWhitespaceRun.m_trailingWhitespace = { };
    trailingWhitespaceRun.m_lastNonWhitespaceContentStart = { };

    m_logicalWidth -= trailingWhitespaceRun.logicalWidth();
    m_textContent->length = leadingNonWhitespaceContentLength;
    m_trailingWhitespace = { };

    return trailingWhitespaceRun;
}

bool Line::Run::hasTrailingLetterSpacing() const
{
    return !hasTrailingWhitespace() && letterSpacing() > 0;
}

InlineLayoutUnit Line::Run::trailingLetterSpacing() const
{
    if (!hasTrailingLetterSpacing())
        return { };
    return InlineLayoutUnit { letterSpacing() };
}

InlineLayoutUnit Line::Run::removeTrailingLetterSpacing()
{
    ASSERT(hasTrailingLetterSpacing());
    auto trailingWidth = trailingLetterSpacing();
    shrinkHorizontally(trailingWidth);
    ASSERT(logicalWidth() > 0 || (!logicalWidth() && letterSpacing() >= static_cast<float>(intMaxForLayoutUnit)));
    return trailingWidth;
}

InlineLayoutUnit Line::Run::removeTrailingWhitespace()
{
    ASSERT(m_trailingWhitespace);
    // According to https://www.w3.org/TR/css-text-3/#white-space-property matrix
    // Trimmable whitespace is always collapsible so the length of the trailing trimmable whitespace is always 1 (or non-existent).
    ASSERT(m_textContent && m_textContent->length);
    constexpr size_t trailingTrimmableContentLength = 1;

    auto trimmedWidth = m_trailingWhitespace->width;
    if (m_lastNonWhitespaceContentStart && inlineDirection() == TextDirection::RTL) {
        // While LTR content could also suffer from slightly incorrect content width after trimming trailing whitespace (see TextUtil::width)
        // it hardly produces visually observable result.
        // FIXME: This may still incorrectly leave some content on the line (vs. re-measuring also at ::expand).
        auto& inlineTextBox = downcast<InlineTextBox>(*m_layoutBox);
        auto startPosition = *m_lastNonWhitespaceContentStart;
        auto endPosition = m_textContent->start + m_textContent->length;
        RELEASE_ASSERT(startPosition < endPosition - trailingTrimmableContentLength);
        if (inlineTextBox.content()[endPosition - 1] == space)
            trimmedWidth = TextUtil::trailingWhitespaceWidth(inlineTextBox, m_style.fontCascade(), startPosition, endPosition);
    }
    m_textContent->length -= trailingTrimmableContentLength;
    m_trailingWhitespace = { };
    shrinkHorizontally(trimmedWidth);
    return trimmedWidth;
}

}
}

#endif
