#include "rive/text/text.hpp"
using namespace rive;
#ifdef WITH_RIVE_TEXT
#include "rive/text_engine.hpp"
#include "rive/component_dirt.hpp"
#include "rive/text/utf.hpp"
#include "rive/text/text_style.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>

void GlyphItr::tryAdvanceRun()
{
    while (true)
    {
        auto run = *m_run;
        if (m_glyphIndex == m_line->endGlyphIndex(run) && run != m_line->lastRun())
        {
            m_run++;
            m_glyphIndex = m_line->startGlyphIndex(*m_run);
        }
        else
        {
            break;
        }
    }
}
GlyphItr& GlyphItr::operator++()
{
    auto run = *m_run;
    m_glyphIndex += run->dir == TextDirection::ltr ? 1 : -1;
    tryAdvanceRun();
    return *this;
}

OrderedLine::OrderedLine(const Paragraph& paragraph,
                         const GlyphLine& line,
                         float lineWidth,
                         bool wantEllipsis,
                         bool isEllipsisLineLast,
                         GlyphRun* ellipsisRun) :
    m_startGlyphIndex(line.startGlyphIndex), m_endGlyphIndex(line.endGlyphIndex)
{
    std::vector<const GlyphRun*> logicalRuns;
    const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;

    if (!wantEllipsis || !buildEllipsisRuns(logicalRuns,
                                            paragraph,
                                            line,
                                            lineWidth,
                                            isEllipsisLineLast,
                                            ellipsisRun))
    {
        for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
        {
            logicalRuns.push_back(&glyphRuns[i]);
        }

        if (!logicalRuns.empty())
        {
            m_startLogical = logicalRuns.front();
            m_endLogical = logicalRuns.back();
        }
    }

    // Now sort the runs visually.
    if (paragraph.baseDirection == TextDirection::ltr || logicalRuns.empty())
    {
        m_runs = std::move(logicalRuns);
    }
    else
    {
        std::vector<const GlyphRun*> visualRuns;
        visualRuns.reserve(logicalRuns.size());

        auto itr = logicalRuns.rbegin();
        auto end = logicalRuns.rend();
        const GlyphRun* first = *itr;
        visualRuns.push_back(first);
        size_t ltrIndex = 0;
        TextDirection previousDirection = first->dir;
        while (++itr != end)
        {
            const GlyphRun* run = *itr;
            if (run->dir == TextDirection::ltr && previousDirection == run->dir)
            {
                visualRuns.insert(visualRuns.begin() + ltrIndex, run);
            }
            else
            {
                if (run->dir == TextDirection::ltr)
                {
                    ltrIndex = visualRuns.size();
                }
                visualRuns.push_back(run);
            }
            previousDirection = run->dir;
        }
        m_runs = std::move(visualRuns);
    }
}

static void appendUnicode(std::vector<rive::Unichar>& unichars, const char text[])
{
    const uint8_t* ptr = (const uint8_t*)text;
    while (*ptr)
    {
        unichars.push_back(rive::UTF::NextUTF8(&ptr));
    }
}

bool OrderedLine::buildEllipsisRuns(std::vector<const GlyphRun*>& logicalRuns,
                                    const Paragraph& paragraph,
                                    const GlyphLine& line,
                                    float lineWidth,
                                    bool isEllipsisLineLast,
                                    GlyphRun* storedEllipsisRun)
{
    float x = 0.0f;
    const SimpleArray<GlyphRun>& glyphRuns = paragraph.runs;
    uint32_t startGIndex = line.startGlyphIndex;
    // If it's the last line we can actually early out if the whole things fits,
    // so check that first with no extra shaping.
    if (isEllipsisLineLast)
    {
        bool fits = true;

        for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
        {
            const GlyphRun& run = glyphRuns[i];
            uint32_t endGIndex =
                i == line.endRunIndex ? line.endGlyphIndex : (uint32_t)run.glyphs.size();

            for (uint32_t j = startGIndex; j != endGIndex; j++)
            {
                x += run.advances[j];
                if (x > lineWidth)
                {
                    fits = false;
                    goto measured;
                }
            }
            startGIndex = 0;
        }
    measured:
        if (fits)
        {
            // It fits, just get the regular glyphs.
            return false;
        }
    }

    std::vector<Unichar> ellipsisCodePoints;
    appendUnicode(ellipsisCodePoints, "...");

    rcp<Font> ellipsisFont = nullptr;
    float ellipsisFontSize = 0.0f;

    GlyphRun ellipsisRun = {};
    float ellipsisWidth = 0.0f;

    bool ellipsisOverflowed = false;
    startGIndex = line.startGlyphIndex;

    for (uint32_t i = line.startRunIndex; i < line.endRunIndex + 1; i++)
    {
        const GlyphRun& run = glyphRuns[i];
        if (run.font != ellipsisFont && run.size != ellipsisFontSize)
        {
            // Track the latest we've checked (even if we discard it so we don't try
            // to do this again for this ellipsis).
            ellipsisFont = run.font;
            ellipsisFontSize = run.size;

            // Get the next shape so we can check if it fits, otherwise keep using
            // the last one.
            TextRun ellipsisRuns[] = {{ellipsisFont,
                                       ellipsisFontSize,
                                       run.lineHeight,
                                       run.letterSpacing,
                                       (uint32_t)ellipsisCodePoints.size()}};
            auto nextEllipsisShape =
                ellipsisFont->shapeText(ellipsisCodePoints, Span<TextRun>(ellipsisRuns, 1));

            // Hard assumption one run and para
            const Paragraph& para = nextEllipsisShape[0];
            const GlyphRun& nextEllipsisRun = para.runs.front();

            float nextEllipsisWidth = 0;
            for (size_t j = 0; j < nextEllipsisRun.glyphs.size(); j++)
            {
                nextEllipsisWidth += nextEllipsisRun.advances[j];
            }

            if (ellipsisRun.font == nullptr || x + nextEllipsisWidth <= lineWidth)
            {
                // This ellipsis still fits, go ahead and use it. Otherwise stick with
                // the old one.
                ellipsisWidth = nextEllipsisWidth;
                ellipsisRun = std::move(para.runs.front());
            }
        }

        uint32_t endGIndex =
            i == line.endRunIndex ? line.endGlyphIndex : (uint32_t)run.glyphs.size();
        for (uint32_t j = startGIndex; j != endGIndex; j++)
        {
            float advance = run.advances[j];
            if (x + advance + ellipsisWidth > lineWidth)
            {
                m_endGlyphIndex = j;
                ellipsisOverflowed = true;
                break;
            }
            x += advance;
        }
        startGIndex = 0;
        logicalRuns.push_back(&run);
        m_endLogical = &run;

        if (ellipsisOverflowed && ellipsisRun.font != nullptr)
        {
            *storedEllipsisRun = std::move(ellipsisRun);
            logicalRuns.push_back(storedEllipsisRun);
            break;
        }
    }

    // There was enough space for it, so let's add the ellipsis (if we didn't
    // already). Note that we already checked if this is the last line and found
    // that the whole text didn't fit.
    if (!ellipsisOverflowed && ellipsisRun.font != nullptr)
    {
        *storedEllipsisRun = std::move(ellipsisRun);
        logicalRuns.push_back(storedEllipsisRun);
    }
    m_startLogical = storedEllipsisRun == logicalRuns.front() ? nullptr : logicalRuns.front();
    return true;
}

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)
{
    if (m_layoutWidth != size.x || m_layoutHeight != size.y)
    {
        m_layoutWidth = size.x;
        m_layoutHeight = size.y;
        markShapeDirty(false);
    }
}

void Text::buildRenderStyles()
{
    for (TextStyle* style : m_renderStyles)
    {
        style->rewindPath();
    }
    m_renderStyles.clear();
    if (m_shape.empty())
    {
        m_bounds = AABB(0.0f, 0.0f, 0.0f, 0.0f);
        return;
    }
    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;
    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] -
                          endRun.letterSpacing;
            if (width > maxWidth)
            {
                maxWidth = width;
            }
            lastLineIndex++;
            if (wantEllipsis && y + line.bottom <= effectiveHeight())
            {
                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;
    }
    isEllipsisLineLast = lastLineIndex == ellipsisLine;

    int lineIndex = 0;
    paragraphIndex = 0;
    switch (effectiveSizing())
    {
        case TextSizing::autoWidth:
            m_bounds = AABB(0.0f, minY, maxWidth, std::max(minY, y - paragraphSpace));
            break;
        case TextSizing::autoHeight:
            m_bounds = AABB(0.0f, minY, effectiveWidth(), std::max(minY, y - paragraphSpace));
            break;
        case TextSizing::fixed:
            m_bounds = AABB(0.0f, minY, effectiveWidth(), minY + effectiveHeight());
            break;
    }

    // Build the clip path if we want it.
    if (overflow() == TextOverflow::clipped)
    {
        if (m_clipRenderPath == nullptr)
        {
            m_clipRenderPath = artboard()->factory()->makeEmptyRenderPath();
        }
        else
        {
            m_clipRenderPath->rewind();
        }

        AABB bounds = localBounds();

        m_clipRenderPath->addRect(bounds.minX, bounds.minY, bounds.width(), bounds.height());
    }
    else
    {
        m_clipRenderPath = nullptr;
    }

    y = -m_bounds.height() * originY();
    if (textOrigin() == TextOrigin::baseline && !m_lines.empty() && !m_lines[0].empty())
    {
        y -= m_lines[0][0].baseline;
    }
    paragraphIndex = 0;

    bool hasModifiers = haveModifiers();
    if (hasModifiers)
    {
        uint32_t textSize = (uint32_t)m_styledText.unichars().size();
        for (TextModifierGroup* modifierGroup : m_modifierGroups)
        {
            modifierGroup->computeCoverage(textSize);
        }
    }
    for (const SimpleArray<GlyphLine>& paragraphLines : m_lines)
    {
        const Paragraph& paragraph = m_shape[paragraphIndex++];
        for (const GlyphLine& line : paragraphLines)
        {
            switch (overflow())
            {
                case TextOverflow::hidden:
                    if (effectiveSizing() == TextSizing::fixed &&
                        y + line.bottom > effectiveHeight())
                    {
                        return;
                    }
                    break;
                case TextOverflow::clipped:
                    if (effectiveSizing() == TextSizing::fixed && y + line.top > effectiveHeight())
                    {
                        return;
                    }
                    break;
                default:
                    break;
            }

            if (lineIndex >= m_orderedLines.size())
            {
                // We need to still compute this line's ordered runs.
                m_orderedLines.emplace_back(OrderedLine(paragraph,
                                                        line,
                                                        effectiveWidth(),
                                                        ellipsisLine == lineIndex,
                                                        isEllipsisLineLast,
                                                        &m_ellipsisRun));
            }

            const OrderedLine& orderedLine = m_orderedLines[lineIndex];
            float x = -m_bounds.width() * originX() + line.startX;
            float renderY = y + line.baseline;
            for (auto glyphItr : orderedLine)
            {
                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);

                uint32_t textIndex = 0;
                uint32_t glyphCount = 0;
                if (hasModifiers)
                {
                    textIndex = run->textIndices[glyphIndex];
                    glyphCount = m_glyphLookup.count(textIndex);

                    float centerX = advance / 2.0f;
                    Mat2D transform =
                        Mat2D::fromScaleAndTranslation(run->size, run->size, -centerX, 0.0f);
                    for (TextModifierGroup* modifierGroup : m_modifierGroups)
                    {
                        float coverage = modifierGroup->glyphCoverage(textIndex, glyphCount);
                        modifierGroup->transform(coverage, transform);
                    }
                    transform =
                        Mat2D::fromTranslate(centerX + x + offset.x, y + line.baseline + offset.y) *
                        transform;

                    path.transformInPlace(transform);
                }
                else
                {
                    path.transformInPlace(
                        Mat2D(run->size, 0.0f, 0.0f, run->size, x + offset.x, renderY + offset.y));
                }

                x += advance;

                assert(run->styleId < m_runs.size());
                TextStyle* style = m_runs[run->styleId]->style();
                // TextValueRun::onAddedDirty botches loading if it cannot
                // resolve a style, so we're confident we have a style here.
                assert(style != nullptr);
                // Consider this the "local" opacity.
                float opacity = 1.0f;
                if (hasModifiers)
                {
                    for (TextModifierGroup* modifierGroup : m_modifierGroups)
                    {
                        if (modifierGroup->modifiesOpacity())
                        {
                            float coverage = modifierGroup->glyphCoverage(textIndex, glyphCount);
                            opacity = modifierGroup->computeOpacity(opacity, coverage);
                        }
                    }
                }
                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());
                }
            }
            if (lineIndex == ellipsisLine)
            {
                return;
            }
            lineIndex++;
        }
        if (!paragraphLines.empty())
        {
            y += paragraphLines.back().bottom;
        }
        y += paragraphSpace;
    }
}

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

void Text::draw(Renderer* renderer)
{
    ClipResult clipResult = applyClip(renderer);
    if (clipResult == ClipResult::noClip)
    {
        // We didn't clip, so make sure to save as we'll be doing some
        // transformations.
        renderer->save();
    }
    if (clipResult != ClipResult::emptyClip)
    {
        renderer->transform(m_WorldTransform);
        if (overflow() == TextOverflow::clipped && m_clipRenderPath)
        {
            renderer->clipPath(m_clipRenderPath.get());
        }
        for (auto style : m_renderStyles)
        {
            style->draw(renderer);
        }
    }
    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(bool sendToLayout)
{
    addDirt(ComponentDirt::Path);
    for (TextModifierGroup* group : m_modifierGroups)
    {
        group->clearRangeMaps();
    }
    markWorldTransformDirty();
#ifdef WITH_RIVE_LAYOUT
    if (sendToLayout)
    {
        for (ContainerComponent* p = parent(); p != nullptr; p = p->parent())
        {
            if (p->is<LayoutComponent>())
            {
                p->as<LayoutComponent>()->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)
{
    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 ? -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::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();
        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 ? -1.0f : effectiveWidth(),
                           (TextAlign)alignValue());
            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 ? -1.0f : effectiveWidth(),
                           (TextAlign)alignValue());
            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 (TextStyle* style : m_renderStyles)
        {
            style->propagateOpacity(renderOpacity());
        }
    }
}

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 lines = BreakLines(shape,
                                std::min(maxSize.x,
                                         sizing() == TextSizing::autoWidth
                                             ? std::numeric_limits<float>::max()
                                             : width()),
                                (TextAlign)alignValue());
        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 -= m_lines[0][0].baseline;
            minY = y;
        }
        int ellipsisLine = -1;
        bool wantEllipsis = overflow() == TextOverflow::ellipsis;

        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] - endRun.letterSpacing;
                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:

        switch (sizing())
        {
            case TextSizing::autoWidth:
                return Vec2D(maxWidth, std::max(minY, computedHeight));
                break;
            case TextSizing::autoHeight:
                return Vec2D(width(), std::max(minY, computedHeight));
                break;
            case TextSizing::fixed:
                return Vec2D(width(), minY + height());
                break;
        }
    }
    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::update(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 TextStyle* 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) {}
#endif