/*
 * Copyright 2019 Google Inc.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "modules/skottie/src/text/TextAdapter.h"

#include "include/core/SkCanvas.h"
#include "include/core/SkContourMeasure.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkM44.h"
#include "include/private/base/SkTPin.h"
#include "modules/skottie/src/SkottieJson.h"
#include "modules/skottie/src/text/RangeSelector.h"
#include "modules/skottie/src/text/TextAnimator.h"
#include "modules/sksg/include/SkSGDraw.h"
#include "modules/sksg/include/SkSGGeometryNode.h"
#include "modules/sksg/include/SkSGGroup.h"
#include "modules/sksg/include/SkSGPaint.h"
#include "modules/sksg/include/SkSGPath.h"
#include "modules/sksg/include/SkSGRect.h"
#include "modules/sksg/include/SkSGRenderEffect.h"
#include "modules/sksg/include/SkSGRenderNode.h"
#include "modules/sksg/include/SkSGTransform.h"
#include "modules/sksg/src/SkSGTransformPriv.h"

// Enable for text layout debugging.
#define SHOW_LAYOUT_BOXES 0

namespace skottie::internal {

namespace {

class GlyphTextNode final : public sksg::GeometryNode {
public:
    explicit GlyphTextNode(Shaper::ShapedGlyphs&& glyphs) : fGlyphs(std::move(glyphs)) {}

    ~GlyphTextNode() override = default;

    const Shaper::ShapedGlyphs* glyphs() const { return &fGlyphs; }

protected:
    SkRect onRevalidate(sksg::InvalidationController*, const SkMatrix&) override {
        return fGlyphs.computeBounds(Shaper::ShapedGlyphs::BoundsType::kConservative);
    }

    void onDraw(SkCanvas* canvas, const SkPaint& paint) const override {
        fGlyphs.draw(canvas, {0,0}, paint);
    }

    void onClip(SkCanvas* canvas, bool antiAlias) const override {
        canvas->clipPath(this->asPath(), antiAlias);
    }

    bool onContains(const SkPoint& p) const override {
        return this->asPath().contains(p.x(), p.y());
    }

    SkPath onAsPath() const override {
        // TODO
        return SkPath();
    }

private:
    const Shaper::ShapedGlyphs fGlyphs;
};

static float align_factor(SkTextUtils::Align a) {
    switch (a) {
        case SkTextUtils::kLeft_Align  : return 0.0f;
        case SkTextUtils::kCenter_Align: return 0.5f;
        case SkTextUtils::kRight_Align : return 1.0f;
    }

    SkUNREACHABLE;
}

} // namespace

class TextAdapter::GlyphDecoratorNode final : public sksg::Group {
public:
    GlyphDecoratorNode(sk_sp<GlyphDecorator> decorator, float scale)
        : fDecorator(std::move(decorator))
        , fScale(scale)
    {}

    ~GlyphDecoratorNode() override = default;

    void updateFragmentData(const std::vector<TextAdapter::FragmentRec>& recs) {
        fFragCount = recs.size();

        SkASSERT(!fFragInfo);
        fFragInfo = std::make_unique<FragmentInfo[]>(recs.size());

        for (size_t i = 0; i < recs.size(); ++i) {
            const auto& rec = recs[i];
            fFragInfo[i] = {rec.fGlyphs, rec.fMatrixNode, rec.fAdvance};
        }

        SkASSERT(!fDecoratorInfo);
        fDecoratorInfo = std::make_unique<GlyphDecorator::GlyphInfo[]>(recs.size());
    }

    SkRect onRevalidate(sksg::InvalidationController* ic, const SkMatrix& ctm) override {
        const auto child_bounds = INHERITED::onRevalidate(ic, ctm);

        for (size_t i = 0; i < fFragCount; ++i) {
            const auto* glyphs = fFragInfo[i].fGlyphs;
            fDecoratorInfo[i].fBounds =
                    glyphs->computeBounds(Shaper::ShapedGlyphs::BoundsType::kTight);
            fDecoratorInfo[i].fMatrix = sksg::TransformPriv::As<SkMatrix>(fFragInfo[i].fMatrixNode);

            fDecoratorInfo[i].fCluster = glyphs->fClusters.empty() ? 0 : glyphs->fClusters.front();
            fDecoratorInfo[i].fAdvance = fFragInfo[i].fAdvance;
        }

        return child_bounds;
    }

    void onRender(SkCanvas* canvas, const RenderContext* ctx) const override {
        auto local_ctx = ScopedRenderContext(canvas, ctx).setIsolation(this->bounds(),
                                                                       canvas->getTotalMatrix(),
                                                                       true);
        this->INHERITED::onRender(canvas, local_ctx);

        fDecorator->onDecorate(canvas, {
            SkSpan(fDecoratorInfo.get(), fFragCount),
            fScale
        });
    }

private:
    struct FragmentInfo {
        const Shaper::ShapedGlyphs* fGlyphs;
        sk_sp<sksg::Matrix<SkM44>>  fMatrixNode;
        float                       fAdvance;
    };

    const sk_sp<GlyphDecorator>                  fDecorator;
    const float                                  fScale;

    std::unique_ptr<FragmentInfo[]>              fFragInfo;
    std::unique_ptr<GlyphDecorator::GlyphInfo[]> fDecoratorInfo;
    size_t                                       fFragCount;

    using INHERITED = Group;
};

// Text path semantics
//
//   * glyphs are positioned on the path based on their horizontal/x anchor point, interpreted as
//     a distance along the path
//
//   * horizontal alignment is applied relative to the path start/end points
//
//   * "Reverse Path" allows reversing the path direction
//
//   * "Perpendicular To Path" determines whether glyphs are rotated to be perpendicular
//      to the path tangent, or not (just positioned).
//
//   * two controls ("First Margin" and "Last Margin") allow arbitrary offseting along the path,
//     depending on horizontal alignement:
//       - left:   offset = first margin
//       - center: offset = first margin + last margin
//       - right:  offset = last margin
//
//   * extranormal path positions (d < 0, d > path len) are allowed
//       - closed path: the position wraps around in both directions
//       - open path: extrapolates from extremes' positions/slopes
//
struct TextAdapter::PathInfo {
    ShapeValue  fPath;
    ScalarValue fPathFMargin       = 0,
                fPathLMargin       = 0,
                fPathPerpendicular = 0,
                fPathReverse       = 0;

    void updateContourData() {
        const auto reverse = fPathReverse != 0;

        if (fPath != fCurrentPath || reverse != fCurrentReversed) {
            // reinitialize cached contour data
            auto path = static_cast<SkPath>(fPath);
            if (reverse) {
                SkPath reversed;
                reversed.reverseAddPath(path);
                path = reversed;
            }

            SkContourMeasureIter iter(path, /*forceClosed = */false);
            fCurrentMeasure  = iter.next();
            fCurrentClosed   = path.isLastContourClosed();
            fCurrentReversed = reverse;
            fCurrentPath     = fPath;

            // AE paths are always single-contour (no moves allowed).
            SkASSERT(!iter.next());
        }
    }

    float pathLength() const {
        SkASSERT(fPath == fCurrentPath);
        SkASSERT((fPathReverse != 0) == fCurrentReversed);

        return fCurrentMeasure ? fCurrentMeasure->length() : 0;
    }

    SkM44 getMatrix(float distance, SkTextUtils::Align alignment) const {
        SkASSERT(fPath == fCurrentPath);
        SkASSERT((fPathReverse != 0) == fCurrentReversed);

        if (!fCurrentMeasure) {
            return SkM44();
        }

        const auto path_len = fCurrentMeasure->length();

        // First/last margin adjustment also depends on alignment.
        switch (alignment) {
            case SkTextUtils::Align::kLeft_Align:   distance += fPathFMargin; break;
            case SkTextUtils::Align::kCenter_Align: distance += fPathFMargin +
                                                                fPathLMargin; break;
            case SkTextUtils::Align::kRight_Align:  distance += fPathLMargin; break;
        }

        // For closed paths, extranormal distances wrap around the contour.
        if (fCurrentClosed) {
            distance = std::fmod(distance, path_len);
            if (distance < 0) {
                distance += path_len;
            }
            SkASSERT(0 <= distance && distance <= path_len);
        }

        SkPoint pos;
        SkVector tan;
        if (!fCurrentMeasure->getPosTan(distance, &pos, &tan)) {
            return SkM44();
        }

        // For open paths, extranormal distances are extrapolated from extremes.
        // Note:
        //   - getPosTan above clamps to the extremes
        //   - the extrapolation below only kicks in for extranormal values
        const auto underflow = std::min(0.0f, distance),
                   overflow  = std::max(0.0f, distance - path_len);
        pos += tan*(underflow + overflow);

        auto m = SkM44::Translate(pos.x(), pos.y());

        // The "perpendicular" flag controls whether fragments are positioned and rotated,
        // or just positioned.
        if (fPathPerpendicular != 0) {
            m = m * SkM44::Rotate({0,0,1}, std::atan2(tan.y(), tan.x()));
        }

        return m;
    }

private:
    // Cached contour data.
    ShapeValue              fCurrentPath;
    sk_sp<SkContourMeasure> fCurrentMeasure;
    bool                    fCurrentReversed = false,
                            fCurrentClosed   = false;
};

sk_sp<TextAdapter> TextAdapter::Make(const skjson::ObjectValue& jlayer,
                                     const AnimationBuilder* abuilder,
                                     sk_sp<SkFontMgr> fontmgr,
                                     sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
                                     sk_sp<Logger> logger) {
    // General text node format:
    // "t": {
    //    "a": [], // animators (see TextAnimator)
    //    "d": {
    //        "k": [
    //            {
    //                "s": {
    //                    "f": "Roboto-Regular",
    //                    "fc": [
    //                        0.42,
    //                        0.15,
    //                        0.15
    //                    ],
    //                    "j": 1,
    //                    "lh": 60,
    //                    "ls": 0,
    //                    "s": 50,
    //                    "t": "text align right",
    //                    "tr": 0
    //                },
    //                "t": 0
    //            }
    //        ]
    //    },
    //    "m": { // more options
    //           "g": 1,     // Anchor Point Grouping
    //           "a": {...}  // Grouping Alignment
    //         },
    //    "p": { // path options
    //           "a": 0,   // force alignment
    //           "f": {},  // first margin
    //           "l": {},  // last margin
    //           "m": 1,   // mask index
    //           "p": 1,   // perpendicular
    //           "r": 0    // reverse path
    //         }

    // },

    const skjson::ObjectValue* jt = jlayer["t"];
    const skjson::ObjectValue* jd = jt ? static_cast<const skjson::ObjectValue*>((*jt)["d"])
                                       : nullptr;
    if (!jd) {
        abuilder->log(Logger::Level::kError, &jlayer, "Invalid text layer.");
        return nullptr;
    }

    // "More options"
    const skjson::ObjectValue* jm = (*jt)["m"];
    static constexpr AnchorPointGrouping gGroupingMap[] = {
        AnchorPointGrouping::kCharacter, // 'g': 1
        AnchorPointGrouping::kWord,      // 'g': 2
        AnchorPointGrouping::kLine,      // 'g': 3
        AnchorPointGrouping::kAll,       // 'g': 4
    };
    const auto apg = jm
            ? SkTPin<int>(ParseDefault<int>((*jm)["g"], 1), 1, std::size(gGroupingMap))
            : 1;

    auto adapter = sk_sp<TextAdapter>(new TextAdapter(std::move(fontmgr),
                                                      std::move(custom_glyph_mapper),
                                                      std::move(logger),
                                                      gGroupingMap[SkToSizeT(apg - 1)]));

    adapter->bind(*abuilder, jd, adapter->fText.fCurrentValue);
    if (jm) {
        adapter->bind(*abuilder, (*jm)["a"], adapter->fGroupingAlignment);
    }

    // Animators
    if (const skjson::ArrayValue* janimators = (*jt)["a"]) {
        adapter->fAnimators.reserve(janimators->size());

        for (const skjson::ObjectValue* janimator : *janimators) {
            if (auto animator = TextAnimator::Make(janimator, abuilder, adapter.get())) {
                adapter->fHasBlurAnimator         |= animator->hasBlur();
                adapter->fRequiresAnchorPoint     |= animator->requiresAnchorPoint();
                adapter->fRequiresLineAdjustments |= animator->requiresLineAdjustments();

                adapter->fAnimators.push_back(std::move(animator));
            }
        }
    }

    // Optional text path
    const auto attach_path = [&](const skjson::ObjectValue* jpath) -> std::unique_ptr<PathInfo> {
        if (!jpath) {
            return nullptr;
        }

        // the actual path is identified as an index in the layer mask stack
        const auto mask_index =
                ParseDefault<size_t>((*jpath)["m"], std::numeric_limits<size_t>::max());
        const skjson::ArrayValue* jmasks = jlayer["masksProperties"];
        if (!jmasks || mask_index >= jmasks->size()) {
            return nullptr;
        }

        const skjson::ObjectValue* mask = (*jmasks)[mask_index];
        if (!mask) {
            return nullptr;
        }

        auto pinfo = std::make_unique<PathInfo>();
        adapter->bind(*abuilder, (*mask)["pt"], &pinfo->fPath);
        adapter->bind(*abuilder, (*jpath)["f"], &pinfo->fPathFMargin);
        adapter->bind(*abuilder, (*jpath)["l"], &pinfo->fPathLMargin);
        adapter->bind(*abuilder, (*jpath)["p"], &pinfo->fPathPerpendicular);
        adapter->bind(*abuilder, (*jpath)["r"], &pinfo->fPathReverse);

        // TODO: force align support

        // Historically, these used to be exported as static properties.
        // Attempt parsing both ways, for backward compat.
        skottie::Parse((*jpath)["p"], &pinfo->fPathPerpendicular);
        skottie::Parse((*jpath)["r"], &pinfo->fPathReverse);

        // Path positioning requires anchor point info.
        adapter->fRequiresAnchorPoint = true;

        return pinfo;
    };

    adapter->fPathInfo = attach_path((*jt)["p"]);

    abuilder->dispatchTextProperty(adapter);

    return adapter;
}

TextAdapter::TextAdapter(sk_sp<SkFontMgr> fontmgr,
                         sk_sp<CustomFont::GlyphCompMapper> custom_glyph_mapper,
                         sk_sp<Logger> logger,
                         AnchorPointGrouping apg)
    : fRoot(sksg::Group::Make())
    , fFontMgr(std::move(fontmgr))
    , fCustomGlyphMapper(std::move(custom_glyph_mapper))
    , fLogger(std::move(logger))
    , fAnchorPointGrouping(apg)
    , fHasBlurAnimator(false)
    , fRequiresAnchorPoint(false)
    , fRequiresLineAdjustments(false) {}

TextAdapter::~TextAdapter() = default;

std::vector<sk_sp<sksg::RenderNode>>
TextAdapter::buildGlyphCompNodes(Shaper::ShapedGlyphs& glyphs) const {
    std::vector<sk_sp<sksg::RenderNode>> draws;

    if (fCustomGlyphMapper) {
        size_t run_offset = 0;
        for (auto& run : glyphs.fRuns) {
            for (size_t i = 0; i < run.fSize; ++i) {
                const size_t goffset = run_offset + i;
                const SkGlyphID  gid = glyphs.fGlyphIDs[goffset];

                if (auto gcomp = fCustomGlyphMapper->getGlyphComp(run.fFont.getTypeface(), gid)) {
                    // Position and scale the "glyph".
                    const auto m = SkMatrix::Translate(glyphs.fGlyphPos[goffset])
                                 * SkMatrix::Scale(fText->fTextSize*fTextShapingScale,
                                                   fText->fTextSize*fTextShapingScale);

                    draws.push_back(sksg::TransformEffect::Make(std::move(gcomp), m));

                    // Remove all related data from the fragment, so we don't attempt to render
                    // this as a regular glyph.
                    SkASSERT(glyphs.fGlyphIDs.size() > goffset);
                    glyphs.fGlyphIDs.erase(glyphs.fGlyphIDs.begin() + goffset);
                    SkASSERT(glyphs.fGlyphPos.size() > goffset);
                    glyphs.fGlyphPos.erase(glyphs.fGlyphPos.begin() + goffset);
                    if (!glyphs.fClusters.empty()) {
                        SkASSERT(glyphs.fClusters.size() > goffset);
                        glyphs.fClusters.erase(glyphs.fClusters.begin() + goffset);
                    }
                    i         -= 1;
                    run.fSize -= 1;
                }
            }
            run_offset += run.fSize;
        }
    }

    return draws;
}

void TextAdapter::addFragment(Shaper::Fragment& frag, sksg::Group* container) {
    // For a given shaped fragment, build a corresponding SG fragment:
    //
    //   [TransformEffect] -> [Transform]
    //     [Group]
    //       [Draw] -> [GlyphTextNode*] [FillPaint]    // SkTypeface-based glyph.
    //       [Draw] -> [GlyphTextNode*] [StrokePaint]  // SkTypeface-based glyph.
    //       [CompRenderTree]                          // Comp glyph.
    //       ...
    //

    FragmentRec rec;
    rec.fOrigin     = frag.fOrigin;
    rec.fAdvance    = frag.fAdvance;
    rec.fAscent     = frag.fAscent;
    rec.fMatrixNode = sksg::Matrix<SkM44>::Make(SkM44::Translate(frag.fOrigin.x(),
                                                                 frag.fOrigin.y()));

    // Start off substituting existing comp nodes for all composition-based glyphs.
    std::vector<sk_sp<sksg::RenderNode>> draws = this->buildGlyphCompNodes(frag.fGlyphs);

    // Use a regular GlyphTextNode for the remaining glyphs (backed by a real SkTypeface).
    auto text_node = sk_make_sp<GlyphTextNode>(std::move(frag.fGlyphs));
    rec.fGlyphs = text_node->glyphs();

    draws.reserve(draws.size() +
                  static_cast<size_t>(fText->fHasFill) +
                  static_cast<size_t>(fText->fHasStroke));

    SkASSERT(fText->fHasFill || fText->fHasStroke);

    auto add_fill = [&]() {
        if (fText->fHasFill) {
            rec.fFillColorNode = sksg::Color::Make(fText->fFillColor);
            rec.fFillColorNode->setAntiAlias(true);
            draws.push_back(sksg::Draw::Make(text_node, rec.fFillColorNode));
        }
    };
    auto add_stroke = [&] {
        if (fText->fHasStroke) {
            rec.fStrokeColorNode = sksg::Color::Make(fText->fStrokeColor);
            rec.fStrokeColorNode->setAntiAlias(true);
            rec.fStrokeColorNode->setStyle(SkPaint::kStroke_Style);
            rec.fStrokeColorNode->setStrokeWidth(fText->fStrokeWidth * fTextShapingScale);
            rec.fStrokeColorNode->setStrokeJoin(fText->fStrokeJoin);
            draws.push_back(sksg::Draw::Make(text_node, rec.fStrokeColorNode));
        }
    };

    if (fText->fPaintOrder == TextPaintOrder::kFillStroke) {
        add_fill();
        add_stroke();
    } else {
        add_stroke();
        add_fill();
    }

    SkASSERT(!draws.empty());

    if (SHOW_LAYOUT_BOXES) {
        // visualize fragment ascent boxes
        auto box_color = sksg::Color::Make(0xff0000ff);
        box_color->setStyle(SkPaint::kStroke_Style);
        box_color->setStrokeWidth(1);
        box_color->setAntiAlias(true);
        auto box = SkRect::MakeLTRB(0, rec.fAscent, rec.fAdvance, 0);
        draws.push_back(sksg::Draw::Make(sksg::Rect::Make(box), std::move(box_color)));
    }

    draws.shrink_to_fit();

    auto draws_node = (draws.size() > 1)
            ? sksg::Group::Make(std::move(draws))
            : std::move(draws[0]);

    if (fHasBlurAnimator) {
        // Optional blur effect.
        rec.fBlur = sksg::BlurImageFilter::Make();
        draws_node = sksg::ImageFilterEffect::Make(std::move(draws_node), rec.fBlur);
    }

    container->addChild(sksg::TransformEffect::Make(std::move(draws_node), rec.fMatrixNode));
    fFragments.push_back(std::move(rec));
}

void TextAdapter::buildDomainMaps(const Shaper::Result& shape_result) {
    fMaps.fNonWhitespaceMap.clear();
    fMaps.fWordsMap.clear();
    fMaps.fLinesMap.clear();

    size_t i          = 0,
           line       = 0,
           line_start = 0,
           word_start = 0;

    float word_advance = 0,
          word_ascent  = 0,
          line_advance = 0,
          line_ascent  = 0;

    bool in_word = false;

    // TODO: use ICU for building the word map?
    for (; i  < shape_result.fFragments.size(); ++i) {
        const auto& frag = shape_result.fFragments[i];

        if (frag.fIsWhitespace) {
            if (in_word) {
                in_word = false;
                fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
            }
        } else {
            fMaps.fNonWhitespaceMap.push_back({i, 1, 0, 0});

            if (!in_word) {
                in_word = true;
                word_start = i;
                word_advance = word_ascent = 0;
            }

            word_advance += frag.fAdvance;
            word_ascent   = std::min(word_ascent, frag.fAscent); // negative ascent
        }

        if (frag.fLineIndex != line) {
            SkASSERT(frag.fLineIndex == line + 1);
            fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
            line = frag.fLineIndex;
            line_start = i;
            line_advance = line_ascent = 0;
        }

        line_advance += frag.fAdvance;
        line_ascent   = std::min(line_ascent, frag.fAscent); // negative ascent
    }

    if (i > word_start) {
        fMaps.fWordsMap.push_back({word_start, i - word_start, word_advance, word_ascent});
    }

    if (i > line_start) {
        fMaps.fLinesMap.push_back({line_start, i - line_start, line_advance, line_ascent});
    }
}

void TextAdapter::setText(const TextValue& txt) {
    fText.fCurrentValue = txt;
    this->onSync();
}

uint32_t TextAdapter::shaperFlags() const {
    uint32_t flags = Shaper::Flags::kNone;

    // We need granular fragments (as opposed to consolidated blobs):
    //   - when animating
    //   - when positioning on a path
    //   - when clamping the number or lines (for accurate line count)
    //   - when a text decorator is present
    if (!fAnimators.empty() || fPathInfo || fText->fMaxLines || fText->fDecorator) {
        flags |= Shaper::Flags::kFragmentGlyphs;
    }

    if (fRequiresAnchorPoint || fText->fDecorator) {
        flags |= Shaper::Flags::kTrackFragmentAdvanceAscent;
    }

    if (fText->fDecorator) {
        flags |= Shaper::Flags::kClusters;
    }

    return flags;
}

void TextAdapter::reshape() {
    // AE clamps the font size to a reasonable range.
    // We do the same, since HB is susceptible to int overflows for degenerate values.
    static constexpr float kMinSize =    0.1f,
                           kMaxSize = 1296.0f;
    const Shaper::TextDesc text_desc = {
        fText->fTypeface,
        SkTPin(fText->fTextSize,    kMinSize, kMaxSize),
        SkTPin(fText->fMinTextSize, kMinSize, kMaxSize),
        SkTPin(fText->fMaxTextSize, kMinSize, kMaxSize),
        fText->fLineHeight,
        fText->fLineShift,
        fText->fAscent,
        fText->fHAlign,
        fText->fVAlign,
        fText->fResize,
        fText->fLineBreak,
        fText->fDirection,
        fText->fCapitalization,
        fText->fMaxLines,
        this->shaperFlags(),
    };
    auto shape_result = Shaper::Shape(fText->fText, text_desc, fText->fBox, fFontMgr);

    if (fLogger) {
        if (shape_result.fFragments.empty() && fText->fText.size() > 0) {
            const auto msg = SkStringPrintf("Text layout failed for '%s'.",
                                            fText->fText.c_str());
            fLogger->log(Logger::Level::kError, msg.c_str());

            // These may trigger repeatedly when the text is animating.
            // To avoid spamming, only log once.
            fLogger = nullptr;
        }

        if (shape_result.fMissingGlyphCount > 0) {
            const auto msg = SkStringPrintf("Missing %zu glyphs for '%s'.",
                                            shape_result.fMissingGlyphCount,
                                            fText->fText.c_str());
            fLogger->log(Logger::Level::kWarning, msg.c_str());
            fLogger = nullptr;
        }
    }

    // Save the text shaping scale for later adjustments.
    fTextShapingScale = shape_result.fScale;

    // Rebuild all fragments.
    // TODO: we can be smarter here and try to reuse the existing SG structure if needed.

    fRoot->clear();
    fFragments.clear();

    if (SHOW_LAYOUT_BOXES) {
        auto box_color = sksg::Color::Make(0xffff0000);
        box_color->setStyle(SkPaint::kStroke_Style);
        box_color->setStrokeWidth(1);
        box_color->setAntiAlias(true);

        auto bounds_color = sksg::Color::Make(0xff00ff00);
        bounds_color->setStyle(SkPaint::kStroke_Style);
        bounds_color->setStrokeWidth(1);
        bounds_color->setAntiAlias(true);

        fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(fText->fBox),
                                         std::move(box_color)));
        fRoot->addChild(sksg::Draw::Make(sksg::Rect::Make(shape_result.computeVisualBounds()),
                                         std::move(bounds_color)));

        if (fPathInfo) {
            auto path_color = sksg::Color::Make(0xffffff00);
            path_color->setStyle(SkPaint::kStroke_Style);
            path_color->setStrokeWidth(1);
            path_color->setAntiAlias(true);

            fRoot->addChild(
                        sksg::Draw::Make(sksg::Path::Make(static_cast<SkPath>(fPathInfo->fPath)),
                                         std::move(path_color)));
        }
    }

    // Depending on whether a GlyphDecorator is present, we either add the glyph render nodes
    // directly to the root group, or to an intermediate GlyphDecoratorNode container.
    sksg::Group* container = fRoot.get();
    sk_sp<GlyphDecoratorNode> decorator_node;
    if (fText->fDecorator) {
        decorator_node = sk_make_sp<GlyphDecoratorNode>(fText->fDecorator, fTextShapingScale);
        container = decorator_node.get();
    }

    // N.B. addFragment moves shaped glyph data out of the fragment, so only the fragment
    // metrics are valid after this block.
    for (size_t i = 0; i < shape_result.fFragments.size(); ++i) {
        this->addFragment(shape_result.fFragments[i], container);
    }

    if (decorator_node) {
        decorator_node->updateFragmentData(fFragments);
        fRoot->addChild(std::move(decorator_node));
    }

    if (!fAnimators.empty() || fPathInfo) {
        // Range selectors and text paths require fragment domain maps.
        this->buildDomainMaps(shape_result);
    }
}

void TextAdapter::onSync() {
    if (!fText->fHasFill && !fText->fHasStroke) {
        return;
    }

    if (fText.hasChanged()) {
        this->reshape();
    }

    if (fFragments.empty()) {
        return;
    }

    // Update the path contour measure, if needed.
    if (fPathInfo) {
        fPathInfo->updateContourData();
    }

    // Seed props from the current text value.
    TextAnimator::ResolvedProps seed_props;
    seed_props.fill_color   = fText->fFillColor;
    seed_props.stroke_color = fText->fStrokeColor;
    seed_props.stroke_width = fText->fStrokeWidth;

    TextAnimator::ModulatorBuffer buf;
    buf.resize(fFragments.size(), { seed_props, 0 });

    // Apply all animators to the modulator buffer.
    for (const auto& animator : fAnimators) {
        animator->modulateProps(fMaps, buf);
    }

    const TextAnimator::DomainMap* grouping_domain = nullptr;
    switch (fAnchorPointGrouping) {
        // for word/line grouping, we rely on domain map info
        case AnchorPointGrouping::kWord: grouping_domain = &fMaps.fWordsMap; break;
        case AnchorPointGrouping::kLine: grouping_domain = &fMaps.fLinesMap; break;
        // remaining grouping modes (character/all) do not need (or have) domain map data
        default: break;
    }

    size_t grouping_span_index = 0;
    SkV2   current_line_offset = { 0, 0 }; // cumulative line spacing

    auto compute_linewide_props = [this](const TextAnimator::ModulatorBuffer& buf,
                                         const TextAnimator::DomainSpan& line_span) {
        SkV2  total_spacing  = {0,0};
        float total_tracking = 0;

        // Only compute these when needed.
        if (fRequiresLineAdjustments && line_span.fCount) {
            for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
                const auto& props = buf[i].props;
                total_spacing  += props.line_spacing;
                total_tracking += props.tracking;
            }

            // The first glyph does not contribute |before| tracking, and the last one does not
            // contribute |after| tracking.
            total_tracking -= 0.5f * (buf[line_span.fOffset].props.tracking +
                                      buf[line_span.fOffset + line_span.fCount - 1].props.tracking);
        }

        return std::make_tuple(total_spacing, total_tracking);
    };

    // Finally, push all props to their corresponding fragment.
    for (const auto& line_span : fMaps.fLinesMap) {
        const auto [line_spacing, line_tracking] = compute_linewide_props(buf, line_span);
        const auto align_offset = -line_tracking * align_factor(fText->fHAlign);

        // line spacing of the first line is ignored (nothing to "space" against)
        if (&line_span != &fMaps.fLinesMap.front() && line_span.fCount) {
            // For each line, the actual spacing is an average of individual fragment spacing
            // (to preserve the "line").
            current_line_offset += line_spacing / line_span.fCount;
        }

        float tracking_acc = 0;
        for (size_t i = line_span.fOffset; i < line_span.fOffset + line_span.fCount; ++i) {
            // Track the grouping domain span in parallel.
            if (grouping_domain && i >= (*grouping_domain)[grouping_span_index].fOffset +
                                        (*grouping_domain)[grouping_span_index].fCount) {
                grouping_span_index += 1;
                SkASSERT(i < (*grouping_domain)[grouping_span_index].fOffset +
                             (*grouping_domain)[grouping_span_index].fCount);
            }

            const auto& props = buf[i].props;
            const auto& frag  = fFragments[i];

            // AE tracking is defined per glyph, based on two components: |before| and |after|.
            // BodyMovin only exports "balanced" tracking values, where before = after = tracking/2.
            //
            // Tracking is applied as a local glyph offset, and contributes to the line width for
            // alignment purposes.
            //
            // No |before| tracking for the first glyph, nor |after| tracking for the last one.
            const auto track_before = i > line_span.fOffset
                                        ? props.tracking * 0.5f : 0.0f,
                       track_after  = i < line_span.fOffset + line_span.fCount - 1
                                        ? props.tracking * 0.5f : 0.0f;

            const auto frag_offset = current_line_offset +
                                     SkV2{align_offset + tracking_acc + track_before, 0};

            tracking_acc += track_before + track_after;

            this->pushPropsToFragment(props, frag, frag_offset, fGroupingAlignment * .01f, // %
                                      grouping_domain ? &(*grouping_domain)[grouping_span_index]
                                                        : nullptr);
        }
    }
}

SkV2 TextAdapter::fragmentAnchorPoint(const FragmentRec& rec,
                                      const SkV2& grouping_alignment,
                                      const TextAnimator::DomainSpan* grouping_span) const {
    // Construct the following 2x ascent box:
    //
    //      -------------
    //     |             |
    //     |             | ascent
    //     |             |
    // ----+-------------+---------- baseline
    //   (pos)           |
    //     |             | ascent
    //     |             |
    //      -------------
    //         advance

    auto make_box = [](const SkPoint& pos, float advance, float ascent) {
        // note: negative ascent
        return SkRect::MakeXYWH(pos.fX, pos.fY + ascent, advance, -2 * ascent);
    };

    // Compute a grouping-dependent anchor point box.
    // The default anchor point is at the center, and gets adjusted relative to the bounds
    // based on |grouping_alignment|.
    auto anchor_box = [&]() -> SkRect {
        switch (fAnchorPointGrouping) {
        case AnchorPointGrouping::kCharacter:
            // Anchor box relative to each individual fragment.
            return make_box(rec.fOrigin, rec.fAdvance, rec.fAscent);
        case AnchorPointGrouping::kWord:
            // Fall through
        case AnchorPointGrouping::kLine: {
            SkASSERT(grouping_span);
            // Anchor box relative to the first fragment in the word/line.
            const auto& first_span_fragment = fFragments[grouping_span->fOffset];
            return make_box(first_span_fragment.fOrigin,
                            grouping_span->fAdvance,
                            grouping_span->fAscent);
        }
        case AnchorPointGrouping::kAll:
            // Anchor box is the same as the text box.
            return fText->fBox;
        }
        SkUNREACHABLE;
    };

    const auto ab = anchor_box();

    // Apply grouping alignment.
    const auto ap = SkV2 { ab.centerX() + ab.width()  * 0.5f * grouping_alignment.x,
                           ab.centerY() + ab.height() * 0.5f * grouping_alignment.y };

    // The anchor point is relative to the fragment position.
    return ap - SkV2 { rec.fOrigin.fX, rec.fOrigin.fY };
}

SkM44 TextAdapter::fragmentMatrix(const TextAnimator::ResolvedProps& props,
                                  const FragmentRec& rec, const SkV2& frag_offset) const {
    const SkV3 pos = {
        props.position.x + rec.fOrigin.fX + frag_offset.x,
        props.position.y + rec.fOrigin.fY + frag_offset.y,
        props.position.z
    };

    if (!fPathInfo) {
        return SkM44::Translate(pos.x, pos.y, pos.z);
    }

    // "Align" the paragraph box left/center/right to path start/mid/end, respectively.
    const auto align_offset =
            align_factor(fText->fHAlign)*(fPathInfo->pathLength() - fText->fBox.width());

    // Path positioning is based on the fragment position relative to the paragraph box
    // upper-left corner:
    //
    //   - the horizontal component determines the distance on path
    //
    //   - the vertical component is post-applied after orienting on path
    //
    // Note: in point-text mode, the box adjustments have no effect as fBox is {0,0,0,0}.
    //
    const auto rel_pos = SkV2{pos.x, pos.y} - SkV2{fText->fBox.fLeft, fText->fBox.fTop};
    const auto path_distance = rel_pos.x + align_offset;

    return fPathInfo->getMatrix(path_distance, fText->fHAlign)
         * SkM44::Translate(0, rel_pos.y, pos.z);
}

void TextAdapter::pushPropsToFragment(const TextAnimator::ResolvedProps& props,
                                      const FragmentRec& rec,
                                      const SkV2& frag_offset,
                                      const SkV2& grouping_alignment,
                                      const TextAnimator::DomainSpan* grouping_span) const {
    const auto anchor_point = this->fragmentAnchorPoint(rec, grouping_alignment, grouping_span);

    rec.fMatrixNode->setMatrix(
                this->fragmentMatrix(props, rec, anchor_point + frag_offset)
              * SkM44::Rotate({ 1, 0, 0 }, SkDegreesToRadians(props.rotation.x))
              * SkM44::Rotate({ 0, 1, 0 }, SkDegreesToRadians(props.rotation.y))
              * SkM44::Rotate({ 0, 0, 1 }, SkDegreesToRadians(props.rotation.z))
              * SkM44::Scale(props.scale.x, props.scale.y, props.scale.z)
              * SkM44::Translate(-anchor_point.x, -anchor_point.y, 0));

    const auto scale_alpha = [](SkColor c, float o) {
        return SkColorSetA(c, SkScalarRoundToInt(o * SkColorGetA(c)));
    };

    if (rec.fFillColorNode) {
        rec.fFillColorNode->setColor(scale_alpha(props.fill_color, props.opacity));
    }
    if (rec.fStrokeColorNode) {
        rec.fStrokeColorNode->setColor(scale_alpha(props.stroke_color, props.opacity));
        rec.fStrokeColorNode->setStrokeWidth(props.stroke_width * fTextShapingScale);
    }
    if (rec.fBlur) {
        rec.fBlur->setSigma({ props.blur.x * kBlurSizeToSigma,
                              props.blur.y * kBlurSizeToSigma });
    }
}

} // namespace skottie::internal
