// Copyright 2021 Google LLC.
#include "experimental/sktext/include/Text.h"
#include "experimental/sktext/src/LogicalRun.h"
#include "experimental/sktext/src/VisualRun.h"
#include <memory>
#include <stack>

using namespace skia_private;

namespace skia {
namespace text {
UnicodeText::UnicodeText(std::unique_ptr<SkUnicode> unicode, SkSpan<uint16_t> utf16)
    : fText16(std::u16string((char16_t*)utf16.data(), utf16.size()))
    , fUnicode(std::move(unicode)) {
    initialize(utf16);
}

UnicodeText::UnicodeText(std::unique_ptr<SkUnicode> unicode, const SkString& utf8)
    : fUnicode(std::move(unicode)) {
    fText16 = fUnicode->convertUtf8ToUtf16(utf8);
    initialize(SkSpan<uint16_t>((uint16_t*)fText16.data(), fText16.size()));
}

bool UnicodeText::isWhitespaces(TextRange range) const {
    for (auto i = range.fStart; i < range.fEnd; ++i) {
        if (!this->hasProperty(i, SkUnicode::CodeUnitFlags::kPartOfWhiteSpaceBreak)) {
            return false;
        }
    }
    return true;
}

void UnicodeText::initialize(SkSpan<uint16_t> utf16) {
    if (!fUnicode) {
        SkASSERT(fUnicode);
        return;
    }

    if (!fUnicode->computeCodeUnitFlags(
                (char16_t*)utf16.data(), utf16.size(), false, &fCodeUnitProperties)) {
        return;
    }
}

std::unique_ptr<FontResolvedText> UnicodeText::resolveFonts(SkSpan<FontBlock> blocks) {

    auto fontResolvedText = std::make_unique<FontResolvedText>();

    TextRange adjustedBlock(0, 0);
    TextIndex index = 0;
    for (auto& block : blocks) {

        index += block.charCount;
        adjustedBlock.fStart = adjustedBlock.fEnd;
        adjustedBlock.fEnd = index;
        if (adjustedBlock.fStart >= adjustedBlock.fEnd) {
            // The last block adjustment went over the entire block
            continue;
        }

        // Move the end of the block to the right until it's on the grapheme edge
        while (adjustedBlock.fEnd < this->fText16.size() &&
               !this->hasProperty(adjustedBlock.fEnd, SkUnicode::CodeUnitFlags::kGraphemeStart)) {
            ++adjustedBlock.fEnd;
        }
        SkASSERT(block.type == BlockType::kFontChain);
        fontResolvedText->resolveChain(this, adjustedBlock, *block.chain);
    }

    std::sort(fontResolvedText->fResolvedFonts.begin(), fontResolvedText->fResolvedFonts.end(),
              [](const ResolvedFontBlock& a, const ResolvedFontBlock& b) {
                return a.textRange.fStart < b.textRange.fStart;
              });
/*
    SkDebugf("Resolved:\n");
    for (auto& f : fontResolvedText->fResolvedFonts) {
        SkDebugf("[%d:%d)\n", f.textRange.fStart, f.textRange.fEnd);
    }
*/
    return fontResolvedText;
}

bool FontResolvedText::resolveChain(UnicodeText* unicodeText, TextRange textRange, const FontChain& fontChain) {

    std::deque<TextRange> unresolvedTexts;
    unresolvedTexts.push_back(textRange);
    for (auto fontIndex = 0; fontIndex < fontChain.count(); ++fontIndex) {
        auto typeface = fontChain[fontIndex];

        std::deque<TextRange> newUnresolvedTexts;
        // Check all text range that have not been resolved yet
        while (!unresolvedTexts.empty()) {
            // Take the first unresolved
            auto unresolvedText = unresolvedTexts.front();
            unresolvedTexts.pop_front();

            // Resolve font for the entire grapheme
            auto start = newUnresolvedTexts.size();
            unicodeText->forEachGrapheme(unresolvedText, [&](TextRange grapheme) {
                auto count = typeface->textToGlyphs(unicodeText->getText16().data() + grapheme.fStart, grapheme.width() * 2, SkTextEncoding::kUTF16, nullptr, 0);
                AutoTArray<SkGlyphID> glyphs(count);
                typeface->textToGlyphs(unicodeText->getText16().data() + grapheme.fStart, grapheme.width() * 2, SkTextEncoding::kUTF16, glyphs.data(), count);
                for (auto i = 0; i < count; ++i) {
                    if (glyphs[i] == 0) {
                        if (newUnresolvedTexts.empty() || newUnresolvedTexts.back().fEnd < grapheme.fStart) {
                            // It's a new unresolved block
                            newUnresolvedTexts.push_back(grapheme);
                        } else {
                            // Let's extend the last unresolved block
                            newUnresolvedTexts.back().fEnd = grapheme.fEnd;
                        }
                        break;
                    }
                }
            });
            // Let's fill the resolved blocks with the current font
            TextRange resolvedText(unresolvedText.fStart, unresolvedText.fStart);
            for (auto newUnresolvedText : newUnresolvedTexts) {
                if (start > 0) {
                    --start;
                    continue;
                }
                resolvedText.fEnd = newUnresolvedText.fStart;
                if (resolvedText.width() > 0) {
                    // Add another resolved block
                    fResolvedFonts.emplace_back(resolvedText, typeface, fontChain.fontSize(), SkFontStyle::Normal());
                }
                resolvedText.fStart = newUnresolvedText.fEnd;
            }
            resolvedText.fEnd = unresolvedText.fEnd;
            if (resolvedText.width() > 0) {
                // Add the last resolved block
                fResolvedFonts.emplace_back(resolvedText, typeface, fontChain.fontSize(), SkFontStyle::Normal());
            }
        }

        // Try the next font in chain
        SkASSERT(unresolvedTexts.empty());
        unresolvedTexts = std::move(newUnresolvedTexts);
    }

    return unresolvedTexts.empty();
}

// Font iterator that finds all formatting marks
// and breaks runs on them (so we can select and interpret them later)
class FormattingFontIterator final : public SkShaper::FontRunIterator {
public:
    FormattingFontIterator(TextIndex textCount,
                           SkSpan<ResolvedFontBlock> fontBlocks,
                           SkSpan<TextIndex> marks)
            : fTextCount(textCount)
            , fFontBlocks(fontBlocks)
            , fFormattingMarks(marks)
            , fCurrentBlock(fontBlocks.begin())
            , fCurrentMark(marks.begin())
            , fCurrentFontIndex(fCurrentBlock->textRange.fEnd) {
        fCurrentFont = this->createFont(*fCurrentBlock);
    }
    void consume() override {
        SkASSERT(fCurrentBlock < fFontBlocks.end());
        SkASSERT(fCurrentMark < fFormattingMarks.end());
        if (fCurrentFontIndex > *fCurrentMark) {
            ++fCurrentMark;
            return;
        }
        if (fCurrentFontIndex == *fCurrentMark) {
            ++fCurrentMark;
        }
        ++fCurrentBlock;
        if (fCurrentBlock < fFontBlocks.end()) {
            fCurrentFontIndex = fCurrentBlock->textRange.fEnd;
            fCurrentFont = this->createFont(*fCurrentBlock);
        }
    }
    size_t endOfCurrentRun() const override {
        SkASSERT(fCurrentMark != fFormattingMarks.end() || fCurrentBlock != fFontBlocks.end());
        if (fCurrentMark == fFormattingMarks.end()) {
            return fCurrentFontIndex;
        } else if (fCurrentBlock == fFontBlocks.end()) {
            return *fCurrentMark;
        } else {
            return std::min(fCurrentFontIndex, *fCurrentMark);
        }
    }
    bool atEnd() const override {
        return (fCurrentBlock == fFontBlocks.end() || fCurrentFontIndex == fTextCount) &&
               (fCurrentMark == fFormattingMarks.end() || *fCurrentMark == fTextCount);
    }
    const SkFont& currentFont() const override { return fCurrentFont; }
    SkFont createFont(const ResolvedFontBlock& resolvedFont) const {
        SkFont font(resolvedFont.typeface, resolvedFont.size);
        font.setEdging(SkFont::Edging::kAntiAlias);
        font.setHinting(SkFontHinting::kSlight);
        font.setSubpixel(true);
        return font;
    }
private:
    TextIndex const fTextCount;
    SkSpan<ResolvedFontBlock> fFontBlocks;
    SkSpan<TextIndex> fFormattingMarks;
    ResolvedFontBlock* fCurrentBlock;
    TextIndex* fCurrentMark;
    TextIndex fCurrentFontIndex;
    SkFont fCurrentFont;
};

std::unique_ptr<ShapedText> FontResolvedText::shape(UnicodeText* unicodeText,
                                                    TextDirection textDirection) {
    // Get utf8 <-> utf16 conversion tables.
    // We need to pass to SkShaper indices in utf8 and then convert them back to utf16 for SkText
    auto text16 = unicodeText->getText16();
    auto text8 = SkUnicode::convertUtf16ToUtf8(std::u16string(text16.data(), text16.size()));
    size_t utf16Index = 0;
    TArray<size_t, true> UTF16FromUTF8;
    TArray<size_t, true> UTF8FromUTF16;
    UTF16FromUTF8.push_back_n(text8.size() + 1, utf16Index);
    UTF8FromUTF16.push_back_n(text16.size() + 1, utf16Index);
    unicodeText->getUnicode()->forEachCodepoint(text8.c_str(), text8.size(),
    [&](SkUnichar unichar, int32_t start, int32_t end, int32_t count) {
        // utf8 index group of 1, 2 or 3 can be represented with one utf16 index group
        for (auto i = start; i < end; ++i) {
            UTF16FromUTF8[i] = utf16Index;
        }
        // utf16 index group of 1 or 2  can refer to the same group of utf8 indices
        for (; count != 0; --count) {
            UTF8FromUTF16[utf16Index++] = start;
        }
    });
    UTF16FromUTF8[text8.size()] = text16.size();
    UTF8FromUTF16[text16.size()] = text8.size();
    // Break text into pieces by font blocks and by formatting marks
    // Formatting marks: \n (and possibly some other later)
    std::vector<size_t> formattingMarks;
    for (size_t i = 0; i < text16.size(); ++i) {
        if (unicodeText->isHardLineBreak(i)) {
            formattingMarks.emplace_back(UTF8FromUTF16[i]);
            formattingMarks.emplace_back(UTF8FromUTF16[i + 1]);
            ++i;
        }
    }
    formattingMarks.emplace_back(text8.size()/* UTF8FromUTF16[text16.size() */);
    // Convert fontBlocks from utf16 to utf8
    TArray<ResolvedFontBlock, true> fontBlocks8;
    for (auto& fb : fResolvedFonts) {
        TextRange text8(UTF8FromUTF16[fb.textRange.fStart], UTF8FromUTF16[fb.textRange.fEnd]);
        fontBlocks8.emplace_back(text8, fb.typeface, fb.size, fb.style);
    }
    auto shapedText = std::make_unique<ShapedText>();
    // Shape the text
    FormattingFontIterator fontIter(text8.size(),
                                    SkSpan<ResolvedFontBlock>(fontBlocks8.data(), fontBlocks8.size()),
                                    SkSpan<TextIndex>(&formattingMarks[0], formattingMarks.size()));
    SkShaper::TrivialLanguageRunIterator langIter(text8.c_str(), text8.size());
    std::unique_ptr<SkShaper::BiDiRunIterator> bidiIter(
        SkShaper::MakeSkUnicodeBidiRunIterator(
            unicodeText->getUnicode(), text8.c_str(), text8.size(), textDirection == TextDirection::kLtr ? 0 : 1));
    std::unique_ptr<SkShaper::ScriptRunIterator> scriptIter(
        SkShaper::MakeSkUnicodeHbScriptRunIterator(text8.c_str(), text8.size()));
    auto shaper = SkShaper::MakeShapeDontWrapOrReorder(unicodeText->getUnicode()->copy());
    if (shaper == nullptr) {
        // For instance, loadICU does not work. We have to stop the process
        return nullptr;
    }
    shaper->shape(
            text8.c_str(), text8.size(),
            fontIter, *bidiIter, *scriptIter, langIter,
            std::numeric_limits<SkScalar>::max(), shapedText.get());
    if (shapedText->fLogicalRuns.empty()) {
        // Create a fake run for an empty text (to avoid all the checks)
        SkShaper::RunHandler::RunInfo emptyInfo {
            fontIter.createFont(fResolvedFonts.front()),
            0,
            SkVector::Make(0.0f, 0.0f),
            0,
            SkShaper::RunHandler::Range(0, 0)
        };
        shapedText->fLogicalRuns.emplace_back(emptyInfo, 0, 0.0f);
        shapedText->fLogicalRuns.front().commit();
    }
    // Fill out all code unit properties
    for (auto& logicalRun : shapedText->fLogicalRuns) {
        // Convert utf8 range into utf16 range
        logicalRun.convertUtf16Range([&](unsigned long index8) {
            return UTF16FromUTF8[index8];
        });
        // Convert all utf8 indexes into utf16 indexes (and also shift them to be on the entire text scale, too)
        logicalRun.convertClusterIndexes([&](TextIndex clusterIndex8) {
            return UTF16FromUTF8[clusterIndex8];
        });
        // Detect and mark line break runs
        if (logicalRun.getTextRange().width() == 1 &&
            logicalRun.size() == 1 &&
            unicodeText->isHardLineBreak(logicalRun.getTextRange().fStart)) {
            logicalRun.setRunType(LogicalRunType::kLineBreak);
        }
    }
    return shapedText;
}

// TODO: Implement the vertical restriction (height) and add ellipsis
std::unique_ptr<WrappedText> ShapedText::wrap(UnicodeText* unicodeText, float width, float height) {
    auto wrappedText = std::unique_ptr<WrappedText>(new WrappedText());
    // line + spaces + clusters
    Stretch line;
    Stretch spaces;
    Stretch clusters;
    Stretch cluster;
    for (size_t runIndex = 0; runIndex < this->fLogicalRuns.size(); ++runIndex ) {
        auto& run = this->fLogicalRuns[runIndex];
        if (run.getRunType() == LogicalRunType::kLineBreak) {
            // This is the end of the word, the end of the line
            if (!clusters.isEmpty()) {
                line.moveTo(spaces);
                line.moveTo(clusters);
                spaces = clusters;
            }
            this->addLine(wrappedText.get(), unicodeText->getUnicode(), line, spaces, true);
            line = spaces;
            clusters = spaces;
            continue;
        }
        TextMetrics runMetrics(run.fFont);

        // Let's wrap the text
        GlyphRange clusterGlyphs;
        DirTextRange clusterText(EMPTY_RANGE, run.leftToRight());
        for (size_t glyphIndex = 0; glyphIndex < run.fPositions.size(); ++glyphIndex) {
            auto textIndex = run.fClusters[glyphIndex];

            if (clusterText == EMPTY_RANGE) {
                // The beginning of a new line (or the first one)
                clusterText = DirTextRange(textIndex, textIndex, run.leftToRight());
                clusterGlyphs = GlyphRange(glyphIndex, glyphIndex);

                Stretch empty(GlyphPos(runIndex, glyphIndex), textIndex, runMetrics);
                line = empty;
                spaces = empty;
                clusters = empty;
                continue;
            }

            if (textIndex == clusterText.fStart) {
                // Skip until the next cluster
                continue;
            }

            // Finish the cluster (notice that it belongs to a single run)
            clusterText.fStart = clusterText.fEnd;
            clusterText.fEnd = textIndex;
            clusterGlyphs.fStart = clusterGlyphs.fEnd;
            clusterGlyphs.fEnd = glyphIndex;
            cluster = Stretch(runIndex, clusterGlyphs, clusterText.normalized(), run.calculateWidth(clusterGlyphs), runMetrics);

            auto isWhitespaces = unicodeText->isWhitespaces(cluster.textRange());
            // line + spaces + clusters + cluster
            if (isWhitespaces) {
                // This is the end of the word
                if (!clusters.isEmpty()) {
                    line.moveTo(spaces);
                    line.moveTo(clusters);
                    spaces = clusters;
                }
                spaces.moveTo(cluster);
                clusters = cluster;
                // Whitespaces do not extend the line width so no wrapping
                continue;
            } else if (!SkScalarIsFinite(width)) {
                // No wrapping - the endless line
                clusters.moveTo(cluster);
                continue;
            }
            // Now let's find out if we can add the cluster to the line
            auto currentWidth = line.width() + spaces.width() + clusters.width() + cluster.width();
            if (currentWidth > width) {
                // Finally, the wrapping case
                if (line.isEmpty()) {
                    if (spaces.isEmpty() && clusters.isEmpty()) {
                        // There is only this cluster and it's too long; we are drawing it anyway
                        line.moveTo(cluster);
                    } else {
                        // We break the only one word on the line by this cluster
                        line.moveTo(clusters);
                    }
                } else {
                  // We move clusters + cluster on the next line
                  // TODO: Parametrise possible ways of breaking too long word
                  //  (start it from a new line or squeeze the part of it on this line)
                }
                this->addLine(wrappedText.get(), unicodeText->getUnicode(), line, spaces, false);
                line = spaces;
            }
            clusters.moveTo(cluster);

            clusterGlyphs.fStart = clusterGlyphs.fEnd;
            clusterText.fStart = clusterText.fEnd;
        }
    }

    // Deal with the last line
    if (!clusters.isEmpty()) {
        line.moveTo(spaces);
        line.moveTo(clusters);
        spaces = clusters;
    } else if (wrappedText->fVisualLines.empty()) {
        // Empty text; we still need a line to avoid checking for empty lines every time
        line.moveTo(cluster);
        spaces.moveTo(cluster);
    }
    this->addLine(wrappedText.get(), unicodeText->getUnicode(), line, spaces, false);
    wrappedText->fActualSize.fWidth = width;
    return wrappedText;
}

TArray<int32_t> ShapedText::getVisualOrder(SkUnicode* unicode, RunIndex startRun, RunIndex endRun) {
    auto numRuns = endRun - startRun + 1;
    TArray<int32_t> results;
    results.push_back_n(numRuns);
    if (numRuns == 0) {
        return results;
    }
    TArray<SkUnicode::BidiLevel> runLevels;
    runLevels.push_back_n(numRuns);
    size_t runLevelsIndex = 0;
    for (RunIndex runIndex = startRun; runIndex <= endRun; ++runIndex) {
        runLevels[runLevelsIndex++] = fLogicalRuns[runIndex].bidiLevel();
    }
    SkASSERT(runLevelsIndex == numRuns);
    unicode->reorderVisual(runLevels.data(), numRuns, results.data());
    return results;
}

// TODO: Fill line fOffset.fY
void ShapedText::addLine(WrappedText* wrappedText, SkUnicode* unicode, Stretch& stretch, Stretch& spaces, bool hardLineBreak) {
    auto spacesStart = spaces.glyphStart();
    Stretch lineStretch = stretch;
    lineStretch.moveTo(spaces);
    auto startRun = lineStretch.glyphStart().runIndex();
    auto endRun = lineStretch.glyphEnd().runIndex();
    // Reorder and cut (if needed) runs so they fit the line
    auto visualOrder = this->getVisualOrder(unicode, startRun, endRun);
    // Walk through the line's runs in visual order
    auto firstRunIndex = startRun;
    auto runStart = wrappedText->fVisualRuns.size();
    SkScalar runOffsetInLine = 0.0f;
    for (auto visualIndex : visualOrder) {
        auto& logicalRun = fLogicalRuns[firstRunIndex + visualIndex];
        if (logicalRun.getRunType() == LogicalRunType::kLineBreak) {
            SkASSERT(false);
        }
        bool isFirstRun = startRun == (firstRunIndex + visualIndex);
        bool isLastRun = endRun == (firstRunIndex + visualIndex);
        bool isSpaceRun = spacesStart.runIndex() == (firstRunIndex + visualIndex);
        auto glyphStart = isFirstRun ? lineStretch.glyphStart().glyphIndex() : 0;
        auto glyphEnd = isLastRun ? lineStretch.glyphEnd().glyphIndex() : logicalRun.size();
        auto glyphSize = glyphEnd - glyphStart;
        auto glyphSpaces = isSpaceRun ? spacesStart.glyphIndex() : glyphEnd;
        if (glyphSpaces > glyphStart) {
            auto textStart = isFirstRun ? lineStretch.textRange().fStart : logicalRun.fUtf16Range.fStart;
            auto textEnd = isLastRun ? lineStretch.textRange().fEnd : logicalRun.fUtf16Range.fEnd;
            wrappedText->fVisualRuns.emplace_back(TextRange(textStart, textEnd),
                                                  glyphSpaces - glyphStart,
                                                  logicalRun.fFont,
                                                  lineStretch.textMetrics().baseline(),
                                                  SkPoint::Make(runOffsetInLine, wrappedText->fActualSize.fHeight),
                                                  logicalRun.leftToRight(),
                                                  SkSpan<SkPoint>(&logicalRun.fPositions[glyphStart], glyphSize + 1),
                                                  SkSpan<SkGlyphID>(&logicalRun.fGlyphs[glyphStart], glyphSize),
                                                  SkSpan<uint32_t>((uint32_t*)&logicalRun.fClusters[glyphStart], glyphSize + 1));
        }
        runOffsetInLine += logicalRun.calculateWidth(glyphStart, glyphEnd);
    }
    auto runRange = wrappedText->fVisualRuns.size() == runStart
                    ? SkSpan<VisualRun>(nullptr, 0)
                    : SkSpan<VisualRun>(&wrappedText->fVisualRuns[runStart], wrappedText->fVisualRuns.size() - runStart);
    wrappedText->fVisualLines.emplace_back(lineStretch.textRange(), hardLineBreak, wrappedText->fActualSize.fHeight, runRange);
    wrappedText->fActualSize.fHeight += lineStretch.textMetrics().height();
    wrappedText->fActualSize.fWidth = std::max(wrappedText->fActualSize.fWidth, lineStretch.width());
    stretch.clean();
    spaces.clean();
}

void WrappedText::format(TextAlign textAlign, TextDirection textDirection) {
    if (fAligned == textAlign) {
        return;
    }
    SkScalar verticalOffset = 0.0f;
    for (auto& line : this->fVisualLines) {
        if (textAlign == TextAlign::kLeft) {
            // Good by default
        } else if (textAlign == TextAlign::kCenter) {
            line.fOffset.fX = (this->fActualSize.width() - line.fActualWidth) / 2.0f;
        } else {
            // TODO: Implement all formatting features
        }
        line.fOffset.fY = verticalOffset;
        verticalOffset += line.fTextMetrics.height();
    }
}

void WrappedText::visit(Visitor* visitor) const {
    size_t lineIndex = 0;
    SkScalar verticalOffset = 0.0f;
    for (auto& line : fVisualLines) {
        visitor->onBeginLine(lineIndex, line.text(), line.isHardBreak(), SkRect::MakeXYWH(0, verticalOffset, line.fActualWidth, line.fTextMetrics.height()));
        // Select the runs that are on the line
        size_t glyphCount = 0ul;
        for (auto& run : line.fRuns) {
            auto diff = line.fTextMetrics.above() - run.fTextMetrics.above();
            SkRect boundingRect = SkRect::MakeXYWH(line.fOffset.fX + run.fPositions[0].fX, line.fOffset.fY + diff, run.width(), run.fTextMetrics.height());
            visitor->onGlyphRun(run.fFont, run.dirTextRange(), boundingRect, run.trailingSpacesStart(),
                                run.size(), run.fGlyphs.data(), run.fPositions.data(), run.fClusters.data());
            glyphCount += run.size();
        }
        visitor->onEndLine(lineIndex, line.text(), line.trailingSpaces(), glyphCount);
        verticalOffset += line.fTextMetrics.height();
        ++lineIndex;
    }
}

std::vector<TextIndex> WrappedText::chunksToBlocks(SkSpan<size_t> chunks) {
    std::vector<TextIndex> blocks;
    blocks.reserve(chunks.size() + 1);
    TextIndex index = 0;
    for (auto chunk : chunks) {
        blocks.emplace_back(index);
        index += chunk;
    }
    blocks.emplace_back(index);
    return blocks;
}

SkSpan<TextIndex> WrappedText::limitBlocks(TextRange textRange, SkSpan<TextIndex> blocks) {
    TextRange limited = EMPTY_RANGE;
    for (auto i = 0ul; i < blocks.size(); ++i) {
        auto block = blocks[i];
        if (textRange.fEnd < block) {
            continue;
        } else if (textRange.fStart >= block) {
            break;
        } else if (limited.fStart == EMPTY_INDEX) {
            limited.fStart = i;
        }
        limited.fEnd = i;
    }

    return SkSpan<TextIndex>(&blocks[textRange.fStart], textRange.width());
}

void WrappedText::visit(UnicodeText* unicodeText, Visitor* visitor, PositionType positionType, SkSpan<size_t> chunks) const {
    // Decor blocks have to be sorted by text cannot intersect but can skip some parts of the text
    // (in which case we use default text style from paragraph style)
    // The edges of the decor blocks don't have to match glyph, grapheme or even unicode code point edges
    // It's out responsibility to adjust them to some reasonable values
    // [a:b) -> [c:d) where
    // c is closest GG cluster edge to a from the left and d is closest GG cluster edge to b from the left
    auto textBlocks = WrappedText::chunksToBlocks(chunks);
    SkScalar verticalOffset = 0.0f;
    LineIndex lineIndex = 0ul;
    size_t glyphCount = 0ul;
    for (auto& line : fVisualLines) {
        visitor->onBeginLine(lineIndex, line.text(), line.isHardBreak(), SkRect::MakeXYWH(0, verticalOffset, line.fActualWidth, line.fTextMetrics.height()));
        RunIndex runIndex = 0ul;
        auto lineBlocks = WrappedText::limitBlocks(line.fText, SkSpan<TextIndex>(textBlocks.data(), textBlocks.size()));
        for (auto& run : fVisualRuns) {
            run.forEachTextBlockInGlyphRange(lineBlocks, [&](DirTextRange dirTextRange) {
                GlyphRange glyphRange = this->textToGlyphs(unicodeText, positionType, runIndex, dirTextRange);
                auto diff = line.fTextMetrics.above() - run.fTextMetrics.above();
                SkRect boundingRect =
                        SkRect::MakeXYWH(line.fOffset.fX + run.fPositions[glyphRange.fStart].fX,
                                         line.fOffset.fY + diff,
                                         run.calculateWidth(glyphRange),
                                         run.fTextMetrics.height());
                visitor->onGlyphRun(run.fFont,
                                    dirTextRange,
                                    boundingRect,
                                    run.trailingSpacesStart(),
                                    glyphRange.width(),
                                    &run.fGlyphs[glyphRange.fStart],
                                    &run.fPositions[glyphRange.fStart],
                                    &run.fClusters[glyphRange.fStart]);
            });
            ++runIndex;
            glyphCount += run.size();
        }
        visitor->onEndLine(lineIndex, line.text(), line.trailingSpaces(), glyphCount);
        verticalOffset += line.fTextMetrics.height();
        ++lineIndex;
    }
}

// TODO: Implement more effective search
GlyphRange WrappedText::textToGlyphs(UnicodeText* unicodeText, PositionType positionType, RunIndex runIndex, DirTextRange dirTextRange) const {
    SkASSERT(runIndex < fVisualRuns.size());
    auto& run = fVisualRuns[runIndex];
    SkASSERT(run.fDirTextRange.contains(dirTextRange));
    GlyphRange glyphRange(0, run.size());
    for (GlyphIndex glyph = 0; glyph < run.size(); ++glyph) {
        auto textIndex = run.fClusters[glyph];
        if (positionType == PositionType::kGraphemeCluster &&
            unicodeText->hasProperty(textIndex, SkUnicode::CodeUnitFlags::kGraphemeStart)) {
            if (dirTextRange.after(textIndex)) {
                glyphRange.fStart = glyph;
            } else if (dirTextRange.before(textIndex)) {
                glyphRange.fEnd = glyph;
            } else {
                return glyphRange;
            }
        }
    }
    SkASSERT(false);
    return glyphRange;
}

std::unique_ptr<SelectableText> WrappedText::prepareToEdit(UnicodeText* unicodeText) const {
    auto selectableText = std::make_unique<SelectableText>();
    this->visit(selectableText.get());
    selectableText->fGlyphUnitProperties.push_back_n(unicodeText->getText16().size() + 1, GlyphUnitFlags::kNoGlyphUnitFlag);
    for (auto index = 0; index < unicodeText->getText16().size(); ++index) {
        if (unicodeText->hasProperty(index, SkUnicode::CodeUnitFlags::kHardLineBreakBefore)) {
            selectableText->fGlyphUnitProperties[index] = GlyphUnitFlags::kGraphemeClusterStart;
        }
    }
    for (const auto& run : fVisualRuns) {
        for (auto& cluster : run.fClusters) {
            if (unicodeText->hasProperty(cluster, SkUnicode::CodeUnitFlags::kGraphemeStart)) {
                selectableText->fGlyphUnitProperties[cluster] = GlyphUnitFlags::kGraphemeClusterStart;
            }
        }
    }
    return selectableText;
}

void SelectableText::onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) {
    SkASSERT(fBoxLines.size() == index);
    fBoxLines.emplace_back(index, lineText, hardBreak, bounds);
}

void SelectableText::onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) {
    auto& line = fBoxLines.back();
    line.fTextEnd = trailingSpaces.fStart;
    line.fTrailingSpacesEnd = trailingSpaces.fEnd;
    SkASSERT(line.fTextByGlyph.size() == glyphCount);
    line.fBoxGlyphs.emplace_back(SkRect::MakeXYWH(line.fBounds.fRight, line.fBounds.fTop, 0.0f, line.fBounds.height()));
    if (line.fTextByGlyph.empty()) {
        // Let's create an empty fake box to avoid all the checks
        line.fTextByGlyph.emplace_back(lineText.fEnd);
    }
    line.fTextByGlyph.emplace_back(lineText.fEnd);
}

void SelectableText::onGlyphRun(const SkFont& font,
                                DirTextRange dirTextRange,
                                SkRect bounds,
                                TextIndex trailingSpaces,
                                size_t glyphCount,
                                const uint16_t glyphs[],
                                const SkPoint positions[],
                                const TextIndex clusters[]) {
    auto& line = fBoxLines.back();
    auto start = line.fTextByGlyph.size();
    line.fBoxGlyphs.push_back_n(glyphCount);
    line.fTextByGlyph.push_back_n(glyphCount);
    for (auto i = 0; i < glyphCount; ++i) {
        auto pos = positions[i];
        auto pos1 = positions[i + 1];
        line.fBoxGlyphs[start + i] = SkRect::MakeXYWH(pos.fX, bounds.fTop, pos1.fX - pos.fX, bounds.height());
        line.fTextByGlyph[start + i] = clusters[i];
    }
}

// TODO: Do something (logN) that is not a linear search
Position SelectableText::findPosition(PositionType positionType, const BoxLine& line, SkScalar x) const {
    Position position(positionType);
    position.fGlyphRange = GlyphRange(0, line.fBoxGlyphs.size() - 1);
    position.fTextRange = line.fTextRange;
    position.fBoundaries.fTop = line.fBounds.fTop;
    position.fBoundaries.fBottom = line.fBounds.fBottom;
    // We look for the narrowest glyph range adjusted to positionType that contains the point.
    // So far we made sure that one unit of any positionType does not cross the run edges
    // Therefore it's going to be represented by a single text range only
    for (; position.fGlyphRange.fStart < position.fGlyphRange.fEnd; ++position.fGlyphRange.fStart) {
        auto glyphBox = line.fBoxGlyphs[position.fGlyphRange.fStart];
        if (glyphBox.fLeft > x) {
            break;
        }
        if (position.fPositionType == PositionType::kGraphemeCluster) {
            auto textIndex = line.fTextByGlyph[position.fGlyphRange.fStart];
            if (this->hasProperty(textIndex, GlyphUnitFlags::kGraphemeClusterStart)) {
                position.fTextRange.fStart = textIndex;
            }
        } else {
            // TODO: Implement
            SkASSERT(false);
        }
    }
    for (; position.fGlyphRange.fEnd > position.fGlyphRange.fStart ; --position.fGlyphRange.fEnd) {
        auto glyphBox = line.fBoxGlyphs[position.fGlyphRange.fStart];
        if (glyphBox.fRight <= x) {
            break;
        }
        if (position.fPositionType == PositionType::kGraphemeCluster) {
            auto textIndex = line.fTextByGlyph[position.fGlyphRange.fEnd];
            if (this->hasProperty(textIndex, GlyphUnitFlags::kGraphemeClusterStart)) {
                position.fTextRange.fEnd = textIndex;
                break;
            }
        } else {
            // TODO: Implement
            SkASSERT(false);
        }
    }
    position.fLineIndex = line.fIndex;
    position.fBoundaries.fLeft = line.fBoxGlyphs[position.fGlyphRange.fStart].fLeft;
    position.fBoundaries.fRight = line.fBoxGlyphs[position.fGlyphRange.fEnd].fRight;
    return position;
}

Position SelectableText::adjustedPosition(PositionType positionType, SkPoint xy) const {
    xy.fX = std::min(xy.fX, this->fActualSize.fWidth);
    xy.fY = std::min(xy.fY, this->fActualSize.fHeight);
    Position position(positionType);
    for (auto& line : fBoxLines) {
        if (line.fBounds.fTop > xy.fY) {
            // We are past the point vertically
            break;
        } else if (line.fBounds.fBottom <= xy.fY) {
            // We haven't reached the point vertically yet
            continue;
        }
        return this->findPosition(positionType, line, xy.fX);
    }
    return this->lastPosition(positionType);
}

Position SelectableText::previousPosition(Position current) const {
    const BoxLine* currentLine = &fBoxLines[current.fLineIndex];
    if (this->isFirstOnTheLine(current)) {
        // Go to the previous line
        if (current.fLineIndex == 0) {
            // We reached the end; there is nowhere to move
            current.fGlyphRange = GlyphRange(0, 0);
            return current;
        } else {
            current.fLineIndex -= 1;
            currentLine = &fBoxLines[current.fLineIndex];
            current.fGlyphRange.fStart = currentLine->fBoxGlyphs.size();
        }
    }
    auto position = this->findPosition(current.fPositionType, *currentLine, currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX());
    if (current.fPositionType == PositionType::kGraphemeCluster) {
        // Either way we found us a grapheme cluster (just make sure of it)
        SkASSERT(this->hasProperty(current.fTextRange.fStart, GlyphUnitFlags::kGraphemeClusterStart));
    }
    return position;
}

Position SelectableText::nextPosition(Position current) const {
    const BoxLine* currentLine = &fBoxLines[current.fLineIndex];
    if (this->isLastOnTheLine(current)) {
        // Go to the next line
        if (current.fLineIndex == this->fBoxLines.size() - 1) {
            // We reached the end; there is nowhere to move
            current.fGlyphRange = GlyphRange(currentLine->fBoxGlyphs.size(), currentLine->fBoxGlyphs.size());
            return current;
        } else {
            current.fLineIndex += 1;
            currentLine = &fBoxLines[current.fLineIndex];
            current.fGlyphRange.fEnd = 0;
        }
    }
    auto position = this->findPosition(current.fPositionType, *currentLine, currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX());
    if (current.fPositionType == PositionType::kGraphemeCluster) {
        // Either way we found us a grapheme cluster (just make sure of it)
        SkASSERT(this->hasProperty(current.fTextRange.fEnd, GlyphUnitFlags::kGraphemeClusterStart));
    }
    return position;
}

Position SelectableText::upPosition(Position current) const {

    if (current.fLineIndex == 0) {
        // We are on the first line; just move to the first position
        return this->firstPosition(current.fPositionType);
    }

    // Go to the previous line
    const BoxLine* currentLine = &fBoxLines[current.fLineIndex];
    auto position = this->findPosition(current.fPositionType, fBoxLines[current.fLineIndex - 1], currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX());
    if (current.fPositionType == PositionType::kGraphemeCluster) {
        // Either way we found us a grapheme cluster (just make sure of it)
        SkASSERT(this->hasProperty(current.fTextRange.fEnd, GlyphUnitFlags::kGraphemeClusterStart));
    }
    return position;
}

Position SelectableText::downPosition(Position current) const {

    if (current.fLineIndex == this->countLines() - 1) {
        // We are on the last line; just move to the last position
        return this->lastPosition(current.fPositionType);
    }

    // Go to the next line
    const BoxLine* currentLine = &fBoxLines[current.fLineIndex];
    auto position = this->findPosition(current.fPositionType, fBoxLines[current.fLineIndex + 1], currentLine->fBoxGlyphs[current.fGlyphRange.fStart].centerX());
    if (current.fPositionType == PositionType::kGraphemeCluster) {
        // Either way we found us a grapheme cluster (just make sure of it)
        SkASSERT(this->hasProperty(current.fTextRange.fEnd, GlyphUnitFlags::kGraphemeClusterStart));
    }
    return position;
}

Position SelectableText::firstPosition(PositionType positionType) const {
    auto firstLine = fBoxLines.front();
    auto firstGlyph = firstLine.fBoxGlyphs.front();
    Position beginningOfText(positionType);
    // Set the glyph range after the last glyph
    beginningOfText.fGlyphRange = GlyphRange { 0, 0};
    beginningOfText.fLineIndex = 0;
    beginningOfText.fBoundaries = SkRect::MakeXYWH(firstGlyph.fLeft, firstGlyph.fTop, 0, firstGlyph.height());
    beginningOfText.fTextRange = this->glyphsToText(beginningOfText);
    beginningOfText.fLineIndex = 0;
    return beginningOfText;
}

Position SelectableText::lastPosition(PositionType positionType) const {
    auto lastLine = fBoxLines.back();
    auto lastGlyph = lastLine.fBoxGlyphs.back();
    Position endOfText(positionType);
    endOfText.fLineIndex = lastLine.fIndex;
    endOfText.fGlyphRange = GlyphRange(lastLine.fBoxGlyphs.size() - 1, lastLine.fBoxGlyphs.size() - 1);
    endOfText.fBoundaries = SkRect::MakeXYWH(lastGlyph.fRight, lastGlyph.fTop, 0, lastGlyph.height());
    endOfText.fTextRange = this->glyphsToText(endOfText);
    endOfText.fLineIndex = lastLine.fIndex;
    return endOfText;
}

Position SelectableText::firstInLinePosition(PositionType positionType, LineIndex lineIndex) const {
    SkASSERT(lineIndex >= 0 && lineIndex < fBoxLines.size());
    auto& line = fBoxLines[lineIndex];
    return this->findPosition(positionType, line, line.fBounds.left());
}

Position SelectableText::lastInLinePosition(PositionType positionType, LineIndex lineIndex) const {
    auto& line = fBoxLines[lineIndex];
    return this->findPosition(positionType, line, line.fBounds.right());
}


TextRange SelectableText::glyphsToText(Position position) const {
    SkASSERT(position.fPositionType != PositionType::kRandomText);
    auto line = this->getLine(position.fLineIndex);
    TextRange textRange = EMPTY_RANGE;
    for (auto glyph = position.fGlyphRange.fStart; glyph <= position.fGlyphRange.fEnd; ++glyph) {
        if (textRange.fStart == EMPTY_INDEX) {
            textRange.fStart = line.fTextByGlyph[glyph];
        }
        textRange.fEnd = line.fTextByGlyph[glyph];
    }
    return textRange;
}
} // namespace text
} // namespace skia
