#include "rive/text/text.hpp"
using namespace rive;
#ifdef WITH_RIVE_TEXT
#include "rive/text_engine.hpp"
#include "rive/component_dirt.hpp"
#include "rive/math/rectangles_to_contour.hpp"
#include "rive/math/transform_components.hpp"
#include "rive/text/text_style_paint.hpp"
#include "rive/text/text_value_run.hpp"
#include "rive/text/text_modifier_group.hpp"
#include "rive/shapes/paint/shape_paint.hpp"
#include "rive/artboard.hpp"
#include "rive/factory.hpp"
#include "rive/clip_result.hpp"
#include <limits>

Vec2D Text::measureLayout(float width,
                          LayoutMeasureMode widthMode,
                          float height,
                          LayoutMeasureMode heightMode)
{
    return measure(Vec2D(widthMode == LayoutMeasureMode::undefined
                             ? std::numeric_limits<float>::max()
                             : width,
                         heightMode == LayoutMeasureMode::undefined
                             ? std::numeric_limits<float>::max()
                             : height));
}

void Text::controlSize(Vec2D size,
                       LayoutScaleType widthScaleType,
                       LayoutScaleType heightScaleType,
                       LayoutDirection direction)
{
    if (m_layoutWidth != size.x || m_layoutHeight != size.y ||
        m_layoutWidthScaleType != (uint8_t)widthScaleType ||
        m_layoutHeightScaleType != (uint8_t)heightScaleType ||
        m_layoutDirection != direction)
    {
        m_layoutWidth = size.x;
        m_layoutHeight = size.y;
        m_layoutWidthScaleType = (uint8_t)widthScaleType;
        m_layoutHeightScaleType = (uint8_t)heightScaleType;
        m_layoutDirection = direction;
        markShapeDirty(false);
    }
}

TextSizing Text::effectiveSizing() const
{
    if (m_layoutWidthScaleType == std::numeric_limits<uint8_t>::max() ||
        m_layoutWidthScaleType == (uint8_t)LayoutScaleType::hug ||
        m_layoutHeightScaleType == (uint8_t)LayoutScaleType::hug)
    {
        return sizing();
    }
    return TextSizing::fixed;
}

void Text::clearRenderStyles()
{
    for (TextStylePaint* style : m_renderStyles)
    {
        style->rewindPath();
    }
    m_renderStyles.clear();

    for (TextValueRun* textValueRun : m_runs)
    {
        textValueRun->resetHitTest();
    }
}

TextBoundsInfo Text::computeBoundsInfo()
{
    const float paragraphSpace = paragraphSpacing();

    // Build up ordered runs as we go.
    int paragraphIndex = 0;
    float y = 0.0f;
    float minY = 0.0f;
    float maxWidth = 0.0f;
    float ellipsedHeight = 0;
    if (textOrigin() == TextOrigin::baseline && !m_lines.empty() &&
        !m_lines[0].empty())
    {
        y -= m_lines[0][0].baseline;
        minY = y;
    }

    int ellipsisLine = -1;
    bool isEllipsisLineLast = false;
    // Find the line to put the ellipsis on (line before the one that
    // overflows).
    bool wantEllipsis = overflow() == TextOverflow::ellipsis &&
                        effectiveSizing() == TextSizing::fixed;

    int lastLineIndex = -1;
    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
    {
        const Paragraph& paragraph = m_shape[paragraphIndex++];
        for (const GlyphLine& line : paragraphLines)
        {
            const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
            const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
            float width = endRun.xpos[line.endGlyphIndex] -
                          startRun.xpos[line.startGlyphIndex];
            if (width > maxWidth)
            {
                maxWidth = width;
            }
            lastLineIndex++;
            if (wantEllipsis && y + line.bottom <= effectiveHeight())
            {
                ellipsedHeight = y + line.bottom;
                ellipsisLine++;
            }
        }

        if (!paragraphLines.empty())
        {
            y += paragraphLines.back().bottom;
        }
        y += paragraphSpace;
    }
    if (wantEllipsis && ellipsisLine == -1)
    {
        // Nothing fits, just show the first line and ellipse it.
        ellipsisLine = 0;
    }
    auto totalHeight = ellipsisLine > 0 ? ellipsedHeight : y;
    isEllipsisLineLast = lastLineIndex == ellipsisLine;
    return {
        minY,
        maxWidth,
        totalHeight,
        ellipsisLine,
        isEllipsisLineLast,
    };
}

LineIter Text::shouldDrawLine(float curY,
                              float totalHeight,
                              const GlyphLine& line)
{
    switch (overflow())
    {
        case TextOverflow::hidden:
            if (effectiveSizing() == TextSizing::fixed)
            {
                switch (verticalAlign())
                {
                    case VerticalTextAlign::top:
                        if (curY + line.bottom > effectiveHeight())
                        {
                            return LineIter::yOutOfBounds;
                        }
                        break;
                    case VerticalTextAlign::middle:
                        if (curY + line.top <
                            totalHeight / 2 - effectiveHeight() / 2)
                        {
                            return LineIter::skipThisLine;
                        }
                        if (curY + line.bottom >
                            totalHeight / 2 + effectiveHeight() / 2)
                        {
                            return LineIter::yOutOfBounds;
                        }
                        break;
                    case VerticalTextAlign::bottom:
                        if (curY + line.top < totalHeight - effectiveHeight())
                        {
                            return LineIter::skipThisLine;
                        }
                        break;
                }
            }
            break;
        case TextOverflow::clipped:
            if (effectiveSizing() == TextSizing::fixed)
            {
                switch (verticalAlign())
                {
                    case VerticalTextAlign::top:
                        if (curY + line.top > effectiveHeight())
                        {
                            return LineIter::yOutOfBounds;
                        }
                        break;
                    case VerticalTextAlign::middle:
                        if (curY + line.bottom <
                            totalHeight / 2 - effectiveHeight() / 2)
                        {
                            return LineIter::skipThisLine;
                        }
                        if (curY + line.top >
                            totalHeight / 2 + effectiveHeight() / 2)
                        {
                            return LineIter::yOutOfBounds;
                        }
                        break;
                    case VerticalTextAlign::bottom:
                        if (curY + line.bottom <
                            totalHeight - effectiveHeight())
                        {
                            return LineIter::skipThisLine;
                        }
                        break;
                }
            }
            break;
        default:
            break;
    }
    return LineIter::drawLine;
}

void Text::buildRenderStyles()
{
    // Step 1: reset stuff
    clearRenderStyles();

    if (m_shape.empty())
    {
        m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f);
        return;
    }

    // Step 2: compute ellipsis information
    TextBoundsInfo info = computeBoundsInfo();
    float minY = info.minY;
    float maxWidth = info.maxWidth;
    float totalHeight = info.totalHeight;
    int ellipsisLine = info.ellipsisLine;
    bool isEllipsisLineLast = info.isEllipsisLineLast;

    // Step 3: update modifiers
    bool hasModifiers = haveModifiers();
    if (hasModifiers)
    {
        uint32_t textSize = (uint32_t)m_styledText.unichars().size();
        for (TextModifierGroup* modifierGroup : m_modifierGroups)
        {
            modifierGroup->computeCoverage(textSize);
            modifierGroup->resetTextFollowPath();
        }
    }

    // Step 4: update bounds
    const float paragraphSpace = paragraphSpacing();
    switch (effectiveSizing())
    {
        case TextSizing::autoWidth:
            m_bounds = AABB(0.0f,
                            minY,
                            maxWidth,
                            std::max(minY, totalHeight - paragraphSpace));
            break;
        case TextSizing::autoHeight:
            m_bounds = AABB(0.0f,
                            minY,
                            effectiveWidth(),
                            std::max(minY, totalHeight - paragraphSpace));
            break;
        case TextSizing::fixed:
            m_bounds =
                AABB(0.0f, minY, effectiveWidth(), minY + effectiveHeight());
            break;
    }

    auto verticalAlignOffset = 0.0f;
    switch (verticalAlign())
    {
        case VerticalTextAlign::middle:
            verticalAlignOffset = (totalHeight - m_bounds.height()) / 2;
            break;
        case VerticalTextAlign::bottom:
            verticalAlignOffset = totalHeight - m_bounds.height();
            break;
        default:
            break;
    }

    // Step 5: Update clip information (if we want it)
    if (overflow() == TextOverflow::clipped)
    {
        m_clipRect.rewind();

        AABB bounds = localBounds();

        float minX = bounds.minX + bounds.width() * originX();
        float minY =
            bounds.minY + bounds.height() * originY() + verticalAlignOffset;
        m_clipRect.addRect(
            AABB(minX, minY, minX + bounds.width(), minY + bounds.height()));
    }

    // Step 6: add the glyphs to render paths
    float curY = minY;
    int lineIndex = 0;
    int paragraphIndex = 0;
    float minX = std::numeric_limits<float>::max();

    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
    {
        const Paragraph& paragraph = m_shape[paragraphIndex++];
        int lineIndexInParagraph = 0;
        for (const GlyphLine& line : paragraphLines)
        {
            LineIter lineIter = shouldDrawLine(curY, totalHeight, line);
            if (lineIter == LineIter::yOutOfBounds)
            {
                goto skipLines;
            }
            else if (lineIter == LineIter::skipThisLine)
            {
                lineIndexInParagraph++;
                lineIndex++;
                continue;
            }

            float renderY = curY + line.baseline;
            // We need to compute this line's ordered runs.
            m_orderedLines.emplace_back(OrderedLine(paragraph,
                                                    line,
                                                    effectiveWidth(),
                                                    ellipsisLine == lineIndex,
                                                    isEllipsisLineLast,
                                                    &m_ellipsisRun,
                                                    renderY));

            float curX = line.startX;
            minX = std::min(curX, minX);
            for (auto glyphItr : m_orderedLines.back())
            {
                const GlyphRun* run = std::get<0>(glyphItr);
                size_t glyphIndex = std::get<1>(glyphItr);

                const Font* font = run->font.get();
                const Vec2D& offset = run->offsets[glyphIndex];

                GlyphID glyphId = run->glyphs[glyphIndex];
                float advance = run->advances[glyphIndex];

                RawPath path = font->getPath(glyphId);

                // Step 6.1: translate to the glyph's origin and scale.
                Vec2D curPos(curX, renderY);
                float centerX = advance / 2.0f;
                TransformComponents tc;
                tc.scaleX(run->size);
                tc.scaleY(run->size);
                tc.x(-centerX);
                Mat2D pathTransform = Mat2D::compose(tc);

                // Step 6.2: apply modifiers on a font-sized glyph
                float opacity = 1.0f;
                if (hasModifiers)
                {
                    uint32_t textIndex = run->textIndices[glyphIndex];
                    uint32_t glyphCount = m_glyphLookup.count(textIndex);

                    for (TextModifierGroup* modifierGroup : m_modifierGroups)
                    {
                        float coverage =
                            modifierGroup->glyphCoverage(textIndex, glyphCount);
                        TransformGlyphArg arg = {
                            curPos,
                            centerX,
                            lineIndexInParagraph,
                            paragraphLines,
                        };
                        modifierGroup->transform(coverage, pathTransform, arg);
                        if (modifierGroup->modifiesOpacity())
                        {
                            opacity = modifierGroup->computeOpacity(opacity,
                                                                    coverage);
                        }
                    }
                }

                // Step 6.3: translate back to center with offset
                pathTransform =
                    Mat2D::fromTranslate(curPos.x + centerX + offset.x,
                                         curPos.y + offset.y) *
                    pathTransform;

                path.transformInPlace(pathTransform);

                assert(run->styleId < m_runs.size());
                TextValueRun* textValueRun = m_runs[run->styleId];
                TextStylePaint* style = textValueRun->style();
                // TextValueRun::onAddedDirty botches loading if it cannot
                // resolve a style, so we're confident we have a style here.
                assert(style != nullptr);

                if (style->addPath(path, opacity))
                {
                    // This was the first path added to the style, so let's
                    // mark it in our draw list.
                    m_renderStyles.push_back(style);
                    style->propagateOpacity(renderOpacity());
                }

                // Bounds of the glyph
                if (textValueRun->isHitTarget())
                {
                    Vec2D topLeft = Vec2D(curX, curY + line.top);
                    Vec2D bottomRight =
                        Vec2D(curX + advance, curY + line.bottom);
                    textValueRun->addHitRect(AABB(topLeft.x,
                                                  topLeft.y,
                                                  bottomRight.x,
                                                  bottomRight.y));
                }
                curX += advance;
            }
            if (lineIndex == ellipsisLine)
            {
                goto skipLines;
            }
            lineIndexInParagraph++;
            lineIndex++;
        }
        if (!paragraphLines.empty())
        {
            curY += paragraphLines.back().bottom;
        }
        curY += paragraphSpacing();
    }
skipLines:
    // Step 7: consider fit mode, and update local transform
    auto scale = 1.0f;
    auto xOffset = -m_bounds.width() * originX();
    auto yOffset = -m_bounds.height() * originY();
    if (overflow() == TextOverflow::fit)
    {
        auto xScale = (effectiveSizing() != TextSizing::autoWidth &&
                       maxWidth > m_bounds.width())
                          ? m_bounds.width() / maxWidth
                          : 1;
        auto baseline = fitFromBaseline() ? m_lines[0][0].baseline : 0;
        auto yScale =
            (effectiveSizing() == TextSizing::fixed &&
             totalHeight > m_bounds.height())
                ? (m_bounds.height() - baseline) / (totalHeight - baseline)
                : 1;
        if (xScale != 1 || yScale != 1)
        {
            scale = std::max(0.0f, xScale > yScale ? yScale : xScale);
            yOffset += baseline * (1 - scale);
            switch (align())
            {
                case TextAlign::center:
                    xOffset += (m_bounds.width() - maxWidth * scale) / 2 -
                               minX * scale;
                    break;
                case TextAlign::right:
                    xOffset +=
                        m_bounds.width() - maxWidth * scale - minX * scale;
                    break;
                default:
                    break;
            }
        }
    }
    if (verticalAlign() != VerticalTextAlign::top)
    {
        if (effectiveSizing() == TextSizing::fixed)
        {
            yOffset = -m_bounds.height() * originY();
            if (verticalAlign() == VerticalTextAlign::middle)
            {
                yOffset += (m_bounds.height() - totalHeight * scale) / 2;
            }
            else if (verticalAlign() == VerticalTextAlign::bottom)
            {
                yOffset += m_bounds.height() - totalHeight * scale;
            }
        }
    }
    m_transform =
        Mat2D::fromScaleAndTranslation(scale, scale, xOffset, yOffset);
#ifdef WITH_RIVE_LAYOUT
    markLayoutNodeDirty();
#endif

    // Step 8: cleanup
    for (TextValueRun* textValueRun : m_runs)
    {
        if (textValueRun->isHitTarget())
        {
            textValueRun->computeHitContours();
        }
    }
}

const TextStylePaint* Text::styleFromShaperId(uint16_t id) const
{
    assert(id < m_runs.size());
    return m_runs[id]->style();
}

void Text::draw(Renderer* renderer)
{
    if (m_needsSaveOperation)
    {
        renderer->save();
    }
    // For now we need to check both empty() and hasRenderPath() in
    // ShapePaintPath because the raw path gets cleared when the render path
    // is created.
    if (overflow() == TextOverflow::clipped &&
        (!m_clipPath.empty() || m_clipPath.hasRenderPath()))
    {
        renderer->clipPath(m_clipPath.renderPath(this));
    }
    auto worldTransform = shapeWorldTransform();
    for (auto style : m_renderStyles)
    {
        style->draw(renderer, worldTransform);
    }
    if (m_needsSaveOperation)
    {
        renderer->restore();
    }
}

void Text::addRun(TextValueRun* run) { m_runs.push_back(run); }

void Text::addModifierGroup(TextModifierGroup* group)
{
    m_modifierGroups.push_back(group);
}

void Text::markShapeDirty() { markShapeDirty(true); }

void Text::markShapeDirty(bool sendToLayout)
{
    addDirt(ComponentDirt::Path);
    for (TextModifierGroup* group : m_modifierGroups)
    {
        group->clearRangeMaps();
    }
    markWorldTransformDirty();
#ifdef WITH_RIVE_LAYOUT
    if (sendToLayout)
    {
        markLayoutNodeDirty();
    }
#endif
}

void Text::modifierShapeDirty() { addDirt(ComponentDirt::Path); }

void Text::markPaintDirty() { addDirt(ComponentDirt::Paint); }

void Text::alignValueChanged() { markShapeDirty(); }

void Text::sizingValueChanged() { markShapeDirty(); }

void Text::overflowValueChanged()
{
    if (effectiveSizing() != TextSizing::autoWidth)
    {
        markShapeDirty();
    }
}

void Text::widthChanged()
{
    if (effectiveSizing() != TextSizing::autoWidth)
    {
        markShapeDirty();
    }
}

void Text::paragraphSpacingChanged() { markPaintDirty(); }

void Text::heightChanged()
{
    if (effectiveSizing() == TextSizing::fixed)
    {
        markShapeDirty();
    }
}

void StyledText::clear()
{
    m_value.clear();
    m_runs.clear();
}

bool StyledText::empty() const { return m_runs.empty(); }

void StyledText::append(rcp<Font> font,
                        float size,
                        float lineHeight,
                        float letterSpacing,
                        const std::string& text,
                        uint16_t styleId)
{
    const uint8_t* ptr = (const uint8_t*)text.c_str();
    uint32_t n = 0;
    while (*ptr)
    {
        m_value.push_back(UTF::NextUTF8(&ptr));
        n += 1;
    }
    m_runs.push_back(
        {std::move(font), size, lineHeight, letterSpacing, n, 0, styleId});
}

bool Text::makeStyled(StyledText& styledText, bool withModifiers) const
{
    styledText.clear();
    uint16_t runIndex = 0;
    for (auto valueRun : m_runs)
    {
        auto style = valueRun->style();
        const std::string& text = valueRun->text();
        if (style == nullptr || style->font() == nullptr || text.empty())
        {
            runIndex++;
            continue;
        }
        styledText.append(style->font(),
                          style->fontSize(),
                          style->lineHeight(),
                          style->letterSpacing(),
                          text,
                          runIndex++);
    }
    if (withModifiers)
    {
        for (TextModifierGroup* group : m_modifierGroups)
        {
            group->applyShapeModifiers(*this, styledText);
        }
    }
    return !styledText.empty();
}

SimpleArray<SimpleArray<GlyphLine>> Text::BreakLines(
    const SimpleArray<Paragraph>& paragraphs,
    float width,
    TextAlign align,
    TextWrap wrap)
{
    bool autoWidth = width == -1.0f;
    float paragraphWidth = width;

    SimpleArray<SimpleArray<GlyphLine>> lines(paragraphs.size());

    size_t paragraphIndex = 0;
    for (auto& para : paragraphs)
    {
        lines[paragraphIndex] = GlyphLine::BreakLines(
            para.runs,
            (autoWidth || wrap == TextWrap::noWrap) ? -1.0f : width);
        if (autoWidth)
        {
            paragraphWidth = std::max(
                paragraphWidth,
                GlyphLine::ComputeMaxWidth(lines[paragraphIndex], para.runs));
        }
        paragraphIndex++;
    }
    paragraphIndex = 0;
    for (auto& para : paragraphs)
    {
        GlyphLine::ComputeLineSpacing(paragraphIndex == 0,
                                      lines[paragraphIndex],
                                      para.runs,
                                      paragraphWidth,
                                      align);
        paragraphIndex++;
    }
    return lines;
}

bool Text::modifierRangesNeedShape() const
{
    for (const TextModifierGroup* modifierGroup : m_modifierGroups)
    {
        if (modifierGroup->needsShape())
        {
            return true;
        }
    }
    return false;
}

void Text::onDirty(ComponentDirt value)
{
    // Sometimes a WorldTransform dirt may also affect Path
    if (hasDirt(value, ComponentDirt::WorldTransform))
    {
        for (TextModifierGroup* modifierGroup : m_modifierGroups)
        {
            modifierGroup->onTextWorldTransformDirty();
        }
    }
    if (hasDirt(value, ComponentDirt::Path | ComponentDirt::Paint))
    {
        for (TextStylePaint* style : m_renderStyles)
        {
            style->invalidateStrokeEffects();
        }
    }
}

void Text::update(ComponentDirt value)
{
    Super::update(value);

    if (hasDirt(value, ComponentDirt::Path))
    {
        // We have modifiers that need shaping we'll need to compute the
        // coverage right before we build the actual shape.
        bool precomputeModifierCoverage = modifierRangesNeedShape();
        bool parentIsLayoutNotArtboard =
            parent()->is<LayoutComponent>() && !parent()->is<Artboard>();
        if (precomputeModifierCoverage &&
            makeStyled(m_modifierStyledText, false))
        {
            auto runs = m_modifierStyledText.runs();
            m_modifierShape =
                runs[0].font->shapeText(m_modifierStyledText.unichars(), runs);
            m_modifierLines =
                BreakLines(m_modifierShape,
                           (effectiveSizing() == TextSizing::autoWidth &&
                            !parentIsLayoutNotArtboard)
                               ? -1.0f
                               : effectiveWidth(),
                           align(),
                           wrap());
            m_glyphLookup.compute(m_modifierStyledText.unichars(),
                                  m_modifierShape);
            uint32_t textSize =
                (uint32_t)m_modifierStyledText.unichars().size();
            for (TextModifierGroup* group : m_modifierGroups)
            {
                group->computeRangeMap(m_modifierStyledText.unichars(),
                                       m_modifierShape,
                                       m_modifierLines,
                                       m_glyphLookup);
                group->computeCoverage(textSize);
            }
        }
        if (makeStyled(m_styledText))
        {
            auto runs = m_styledText.runs();
            m_shape = runs[0].font->shapeText(m_styledText.unichars(), runs);

            m_lines = BreakLines(m_shape,
                                 (effectiveSizing() == TextSizing::autoWidth &&
                                  !parentIsLayoutNotArtboard)
                                     ? -1.0f
                                     : effectiveWidth(),
                                 align(),
                                 wrap());
            if (!precomputeModifierCoverage && haveModifiers())
            {
                m_glyphLookup.compute(m_styledText.unichars(), m_shape);
                uint32_t textSize = (uint32_t)m_styledText.unichars().size();
                for (TextModifierGroup* group : m_modifierGroups)
                {
                    group->computeRangeMap(m_styledText.unichars(),
                                           m_shape,
                                           m_lines,
                                           m_glyphLookup);
                    group->computeCoverage(textSize);
                }
            }
        }
        else
        {
            m_shape = SimpleArray<Paragraph>();
            m_lines = SimpleArray<SimpleArray<GlyphLine>>();
            m_glyphLookup.clear();
        }
        m_orderedLines.clear();
        m_ellipsisRun = {};

        // Immediately build render styles so dimensions get computed.
        buildRenderStyles();
    }
    else if (hasDirt(value, ComponentDirt::Paint))
    {
        buildRenderStyles();
    }
    else if (hasDirt(value, ComponentDirt::RenderOpacity))
    {
        // Note that buildRenderStyles does this too, which is why we can get
        // away doing this in the else.
        for (TextStylePaint* style : m_renderStyles)
        {
            style->propagateOpacity(renderOpacity());
        }
    }

    if (hasDirt(value,
                ComponentDirt::WorldTransform | ComponentDirt::Path |
                    ComponentDirt::Paint))
    {
        m_clipPath.rewind();
        m_shapeWorldTransform = m_WorldTransform * m_transform;
        m_clipPath.addPath(m_clipRect, &m_shapeWorldTransform);
    }
}

Vec2D Text::measure(Vec2D maxSize)
{
    if (makeStyled(m_styledText))
    {
        const float paragraphSpace = paragraphSpacing();
        auto runs = m_styledText.runs();
        auto shape = runs[0].font->shapeText(m_styledText.unichars(), runs);
        auto measuringWidth = 0.0f;
        switch (effectiveSizing())
        {
            case TextSizing::autoHeight:
            case TextSizing::fixed:
                measuringWidth = width();
                break;
            default:
                measuringWidth = std::numeric_limits<float>::max();
                break;
        }
        auto measuringWrap = maxSize.x == std::numeric_limits<float>::max() &&
                                     effectiveSizing() != TextSizing::autoHeight
                                 ? TextWrap::noWrap
                                 : wrap();
        auto lines = BreakLines(shape,
                                std::min(maxSize.x, measuringWidth),
                                align(),
                                measuringWrap);
        float y = 0;
        float computedHeight = 0.0f;
        float minY = 0;
        int paragraphIndex = 0;
        float maxWidth = 0;

        if (textOrigin() == TextOrigin::baseline && !lines.empty() &&
            !lines[0].empty())
        {
            y -= lines[0][0].baseline;
            minY = y;
        }
        int ellipsisLine = -1;
        bool wantEllipsis = overflow() == TextOverflow::ellipsis &&
                            sizing() == TextSizing::fixed;

        for (const SimpleArray<GlyphLine>& paragraphLines : lines)
        {
            const Paragraph& paragraph = shape[paragraphIndex++];
            for (const GlyphLine& line : paragraphLines)
            {
                const GlyphRun& endRun = paragraph.runs[line.endRunIndex];
                const GlyphRun& startRun = paragraph.runs[line.startRunIndex];
                float width = endRun.xpos[line.endGlyphIndex] -
                              startRun.xpos[line.startGlyphIndex];
                if (width > maxWidth)
                {
                    maxWidth = width;
                }
                if (wantEllipsis && y + line.bottom > maxSize.y)
                {
                    if (ellipsisLine == -1)
                    {
                        // Nothing fits, just show the first line and ellipse
                        // it.
                        computedHeight = y + line.bottom;
                    }
                    goto doneMeasuring;
                }
                ellipsisLine++;
                computedHeight = y + line.bottom;
            }
            if (!paragraphLines.empty())
            {
                y += paragraphLines.back().bottom;
            }
            y += paragraphSpace;
        }
    doneMeasuring:
        Vec2D bounds;
        switch (sizing())
        {
            case TextSizing::autoWidth:
                bounds = Vec2D(maxWidth, std::max(minY, computedHeight));
                break;
            case TextSizing::autoHeight:
                bounds = Vec2D(width(), std::max(minY, computedHeight));
                break;
            case TextSizing::fixed:
                bounds = Vec2D(width(), minY + height());
                break;
        }
        return Vec2D(std::min(maxSize.x, bounds.x),
                     std::min(maxSize.y, bounds.y));
    }
    return Vec2D();
}

AABB Text::localBounds() const
{
    float width = m_bounds.width();
    float height = m_bounds.height();
    return AABB::fromLTWH(m_bounds.minX - width * originX(),
                          m_bounds.minY - height * originY(),
                          width,
                          height);
}

Core* Text::hitTest(HitInfo*, const Mat2D&)
{
    if (renderOpacity() == 0.0f)
    {
        return nullptr;
    }

    return nullptr;
}

void Text::originValueChanged()
{
    markPaintDirty();
    markWorldTransformDirty();
}

void Text::originXChanged()
{
    markPaintDirty();
    markWorldTransformDirty();
}
void Text::originYChanged()
{
    markPaintDirty();
    markWorldTransformDirty();
}

#else
// Text disabled.
void Text::draw(Renderer* renderer) {}
Core* Text::hitTest(HitInfo*, const Mat2D&) { return nullptr; }
void Text::addRun(TextValueRun* run) {}
void Text::addModifierGroup(TextModifierGroup* group) {}
void Text::markShapeDirty(bool sendToLayout) {}
void Text::markShapeDirty() {}
void Text::update(ComponentDirt value) {}
void Text::onDirty(ComponentDirt value) {}
void Text::alignValueChanged() {}
void Text::sizingValueChanged() {}
void Text::overflowValueChanged() {}
void Text::widthChanged() {}
void Text::heightChanged() {}
void Text::markPaintDirty() {}
void Text::modifierShapeDirty() {}
bool Text::modifierRangesNeedShape() const { return false; }
const TextStylePaint* Text::styleFromShaperId(uint16_t id) const
{
    return nullptr;
}
void Text::paragraphSpacingChanged() {}
AABB Text::localBounds() const { return AABB(); }
void Text::originValueChanged() {}
void Text::originXChanged() {}
void Text::originYChanged() {}
Vec2D Text::measureLayout(float width,
                          LayoutMeasureMode widthMode,
                          float height,
                          LayoutMeasureMode heightMode)
{
    return Vec2D();
}
void Text::controlSize(Vec2D size,
                       LayoutScaleType widthScaleType,
                       LayoutScaleType heightScaleType,
                       LayoutDirection direction)
{}
#endif

TextAlign Text::align() const
{
    auto val = (TextAlign)alignValue();
    if (m_layoutDirection == LayoutDirection::inherit ||
        val == TextAlign::center)
    {
        return val;
    }
    return m_layoutDirection == LayoutDirection::ltr ? TextAlign::left
                                                     : TextAlign::right;
}