// Copyright 2021 Google LLC.
#ifndef Text_DEFINED
#define Text_DEFINED
#include <string>
#include "experimental/sktext/include/Types.h"
#include "experimental/sktext/src/Line.h"
#include "experimental/sktext/src/LogicalRun.h"
#include "experimental/sktext/src/VisualRun.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkFontMgr.h"
#include "include/core/SkFontStyle.h"
#include "include/core/SkPaint.h"
#include "include/core/SkSize.h"
#include "include/core/SkString.h"
#include "include/core/SkTextBlob.h"
#include "modules/skshaper/include/SkShaper.h"
#include "modules/skunicode/include/SkUnicode.h"

namespace skia {
namespace text {

class FontResolvedText;

/**
 * This class contains all the SKUnicode/ICU information.
 */
class UnicodeText {
public:
    /** Makes calls to SkShaper and collects all the shaped data.
        @param blocks         a range of FontBlock elements that keep information about
                              fonts required to shape the text.
                              It's utf16 range but internally it will have to be converted
                              to utf8 (since all shaping operations use utf8 encoding)
        @param textDirection  a starting text direction value
        @return               an object that contains the result of shaping operations
    */
    std::unique_ptr<FontResolvedText> resolveFonts(SkSpan<FontBlock> blocks);

    UnicodeText(std::unique_ptr<SkUnicode> unicode, SkSpan<uint16_t> utf16);
    UnicodeText(std::unique_ptr<SkUnicode> unicode, const SkString& utf8);
    ~UnicodeText() = default;

    bool hasProperty(TextIndex index, SkUnicode::CodeUnitFlags flag) const {
        return (fCodeUnitProperties[index] & flag) == flag;
    }
    bool isHardLineBreak(TextIndex index) const {
        return this->hasProperty(index, SkUnicode::CodeUnitFlags::kHardLineBreakBefore);
    }
    bool isSoftLineBreak(TextIndex index) const {
        return index != 0 && this->hasProperty(index, SkUnicode::CodeUnitFlags::kSoftLineBreakBefore);
    }
    bool isWhitespaces(TextRange range) const;

    SkUnicode* getUnicode() const { return fUnicode.get(); }
    SkSpan<const char16_t> getText16() const { return SkSpan<const char16_t>(fText16.data(), fText16.size()); }

    template <typename Callback>
    void forEachGrapheme(TextRange textRange, Callback&& callback) {
        TextRange grapheme(textRange.fStart, textRange.fStart);
        for (size_t i = textRange.fStart; i < textRange.fEnd; ++i) {
            if (this->hasProperty(i, SkUnicode::CodeUnitFlags::kGraphemeStart)) {
                grapheme.fEnd = i;
                if (grapheme.width() > 0) {
                    callback(grapheme);
                }
                grapheme.fStart = grapheme.fEnd;
            }  else if (this->hasProperty(i, SkUnicode::CodeUnitFlags::kHardLineBreakBefore)) {
                grapheme.fEnd = i;
                callback(grapheme);
                // TODO: We assume here that the line break takes only one codepoint
                // Skip the next line
                grapheme.fStart = grapheme.fEnd + 1;
            }
        }
        grapheme.fEnd = textRange.fEnd;
        if (grapheme.width() > 0) {
            callback(grapheme);
        }
    }

private:
    void initialize(SkSpan<uint16_t> utf16);

    SkTArray<SkUnicode::CodeUnitFlags, true> fCodeUnitProperties;
    std::u16string fText16;
    std::unique_ptr<SkUnicode> fUnicode;
};

class ShapedText;
/**
 * This class contains provides functionality for resolving fonts
 */
class FontResolvedText {
public:
    /** Makes calls to SkShaper and collects all the shaped data.
        @param blocks         a range of FontBlock elements that keep information about
                              fonts required to shape the text.
                              It's utf16 range but internally it will have to be converted
                              to utf8 (since all shaping operations use utf8 encoding)
        @param textDirection  a starting text direction value
        @return               an object that contains the result of shaping operations
    */
    virtual std::unique_ptr<ShapedText> shape(UnicodeText* unicodeText, TextDirection textDirection);

    FontResolvedText() = default;
    virtual ~FontResolvedText() = default;

    bool resolveChain(UnicodeText* unicodeText, TextRange textRange, const FontChain& fontChain);
    SkSpan<ResolvedFontBlock> resolvedFonts() { return SkSpan<ResolvedFontBlock>(fResolvedFonts.data(), fResolvedFonts.size()); }
private:
    friend class UnicodeText;
    SkTArray<ResolvedFontBlock, true> fResolvedFonts;
};

class WrappedText;
/**
 * This class provides all the information from SkShaper/harfbuzz in a raw format.
 * It does require a single existing font for each codepoint.
 */
 // Question: do we provide a visitor for ShapedText?
class ShapedText : public SkShaper::RunHandler {
public:
    /** Break text by lines with a given width (and possible new lines).
        @param unicodeText    a reference to UnicodeText that is used to query Unicode information
        @param width          a line width at which the text gets wrapped
        @param height         a text height, currently not supported
        @return               an object that contains the result of shaping operations (wrapping and formatting).
    */
    std::unique_ptr<WrappedText> wrap(UnicodeText* unicodeText, float width, float height);

    ShapedText()
    : fCurrentRun(nullptr)
    , fParagraphTextStart(0)
    , fRunGlyphStart(0.0f) { }

    void beginLine() override {}
    void runInfo(const RunInfo&) override {}
    void commitRunInfo() override {}
    void commitLine() override {}
    void commitRunBuffer(const RunInfo&) override {
        fCurrentRun->commit();
        fLogicalRuns.emplace_back(std::move(*fCurrentRun));
        fRunGlyphStart += fCurrentRun->width();
    }
    Buffer runBuffer(const RunInfo& info) override {
        fCurrentRun = std::make_unique<LogicalRun>(info, fParagraphTextStart, fRunGlyphStart);
        return fCurrentRun->newRunBuffer();
    }

    SkSpan<const LogicalRun> getLogicalRuns() const { return SkSpan<const LogicalRun>(fLogicalRuns.begin(), fLogicalRuns.size()); }
private:
    friend class FontResolvedText;

    void addLine(WrappedText* wrappedText, SkUnicode* unicode, Stretch& stretch, Stretch& spaces, bool hardLineBreak);

    SkTArray<int32_t> getVisualOrder(SkUnicode* unicode, RunIndex start, RunIndex end);

    // This is all the results from shaping
    SkTArray<LogicalRun, false> fLogicalRuns;

    // Temporary values
    std::unique_ptr<LogicalRun> fCurrentRun;
    TextIndex fParagraphTextStart;
    SkScalar fRunGlyphStart;
};

/**
 * This is a helper visitor class that allows a user to process the wrapped text
 * structures: lines and runs (to draw them, for instance)
 */
class Visitor {
public:
    virtual ~Visitor() = default;
    virtual void onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) { }
    virtual void onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) { }
    virtual void onGlyphRun(const SkFont& font,
                            DirTextRange dirTextRange,        // Currently we make sure that the run edges are the grapheme cluster edges
                            SkRect bounds,                    // bounds contains the physical boundaries of the run
                            TextIndex trailingSpaces,         // Depending of TextDirection it goes right to the end (LTR) or left to the start (RTL)
                            size_t glyphCount,                // Just the number of glyphs
                            const uint16_t glyphs[],          // GlyphIDs from the font
                            const SkPoint positions[],        // Positions relative to the line
                            const TextIndex clusters[])       // Text indices inside the entire text
    { }
    virtual void onPlaceholder(TextRange, const SkRect& bounds) { }
};

class DrawableText;
class SelectableText;
/**
 * This class provides all the information about wrapped/formatted text.
 */
class WrappedText {
public:
    /** Builds a list of SkTextBlobs to draw on a canvas.
        @param positionType   specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
        @param blocks         a range of text indices that cause an additional run breaking to be used for styling
        @return               an object that contains a list of SkTextBlobs to draw on a canvas
    */
    template <class Drawable>
    std::unique_ptr<Drawable> prepareToDraw(UnicodeText* unicodeText, PositionType positionType, SkSpan<TextIndex> blocks) const {
        auto drawableText = std::make_unique<Drawable>();
        this->visit(unicodeText, drawableText.get(), positionType, blocks);
        return drawableText;
    }
    /** Aggregates all the data to navigate the text (move up, down, left, right),
        select some text near the cursor point, adjust all text position to word,
        grapheme cluster and such.
        @return               an object that contains all the data for navigation
    */
    std::unique_ptr<SelectableText> prepareToEdit(UnicodeText* unicodeText) const;
    /** Formats a text line by line.
        @param textAlign     specifies a text placement on the line:
                             left, right, center and justified (last one currently not supported)
        @param textDirection specifies a text direction that also used in formatting
    */
    void format(TextAlign textAlign, TextDirection textDirection);

    SkSize actualSize() const { return fActualSize; }
    size_t countLines() const { return fVisualLines.size(); }

    /** Walks though the data structures and makes certain callbacks on visitor so the visitor can collect all the information.
        @param visitor      a reference to Visitor object
    */
    void visit(Visitor* visitor) const;
    /** Walks though the data structures and makes certain callbacks on visitor so the visitor can collect all the information.
        @param unicodeText  a reference to UnicodeText object
        @param visitor      a reference to Visitor object
        @param positionType specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
                            to map text blocks to glyph ranges.
        @param chunks       a range of widths that cause an additional run breaking to be used for styling
    */
    void visit(UnicodeText* unicodeText, Visitor* visitor, PositionType positionType, SkSpan<size_t> chunks) const;

    static std::vector<TextIndex> chunksToBlocks(SkSpan<size_t> chunks);
    static SkSpan<TextIndex> limitBlocks(TextRange textRange, SkSpan<TextIndex> blocks);

private:
    friend class ShapedText;
    WrappedText() : fActualSize(SkSize::MakeEmpty()), fAligned(TextAlign::kNothing) { }
    GlyphRange textToGlyphs(UnicodeText* unicodeText, PositionType positionType, RunIndex runIndex, DirTextRange dirTextRange) const;
    SkTArray<VisualRun, true> fVisualRuns;    // Broken by lines
    SkTArray<VisualLine, false> fVisualLines;
    SkSize fActualSize;
    TextAlign fAligned;
};

/** This class contains all the data that allows easily paint the text on canvas.
    Strictly speaking, it is not an important part of SkText API but
    it presents a good example of SkText usages and simplifies testing.
*/
class DrawableText : public Visitor {
public:
    DrawableText() = default;

    void onGlyphRun(const SkFont& font,
                    DirTextRange dirTextRange,
                    SkRect bounds,
                    TextIndex trailingSpaces,
                    size_t glyphCount,
                    const uint16_t glyphs[],
                    const SkPoint positions[],
                    const TextIndex clusters[]) override {
        SkTextBlobBuilder builder;
        const auto& blobBuffer = builder.allocRunPos(font, SkToInt(glyphCount));
        sk_careful_memcpy(blobBuffer.glyphs, glyphs, glyphCount * sizeof(uint16_t));
        sk_careful_memcpy(blobBuffer.points(), positions, glyphCount * sizeof(SkPoint));
        fTextBlobs.emplace_back(builder.make());
    }
    std::vector<sk_sp<SkTextBlob>>& getTextBlobs() { return fTextBlobs; }
private:
    std::vector<sk_sp<SkTextBlob>> fTextBlobs;
};

struct Position {
    Position(PositionType positionType, size_t lineIndex, GlyphRange glyphRange, TextRange textRange, SkRect rect)
        : fPositionType(positionType)
        , fLineIndex(lineIndex)
        , fGlyphRange(glyphRange)
        , fTextRange(textRange)
        , fBoundaries(rect) { }

    Position(PositionType positionType)
        : Position(positionType, EMPTY_INDEX, EMPTY_RANGE, EMPTY_RANGE, SkRect::MakeEmpty()) { }

    PositionType fPositionType;
    size_t fLineIndex;
    GlyphRange fGlyphRange;
    TextRange fTextRange;
    SkRect fBoundaries;
};

struct BoxLine {
    BoxLine(size_t index, TextRange text, bool hardBreak, SkRect bounds)
        : fTextRange(text), fIndex(index), fIsHardBreak(hardBreak), fBounds(bounds) { }
    SkTArray<SkRect, true> fBoxGlyphs;
    SkTArray<TextIndex, true> fTextByGlyph; // by glyph cluster
    GlyphIndex fTextEnd;
    GlyphIndex fTrailingSpacesEnd;
    TextRange fTextRange;
    size_t fIndex;
    bool fIsHardBreak;
    SkRect fBounds;
};

/** This class contains all the data that allows all navigation operations on the text:
    move up/down/left/right, select some units of text and such.
*/
class SelectableText : public Visitor {
public:
    SelectableText() = default;

    /** Find the drawable unit (specified by positionType) closest to the screen point
        @param positionType   specifies a text adjustment granularity (grapheme cluster, grapheme, glypheme, glyph)
        @param point          a physical coordinates on a screen to find the closest glyph
        @return               a position object that contains all required information
    */
    Position adjustedPosition(PositionType positionType, SkPoint point) const;

    Position previousPosition(Position current) const;
    Position nextPosition(Position current) const;
    Position upPosition(Position current) const;
    Position downPosition(Position current) const;
    Position firstPosition(PositionType positionType) const;
    Position lastPosition(PositionType positionType) const;
    Position firstInLinePosition(PositionType positionType, LineIndex lineIndex) const;
    Position lastInLinePosition(PositionType positionType, LineIndex lineIndex) const;

    bool isFirstOnTheLine(Position element) const {
        return (element.fGlyphRange.fStart == 0);
    }
    bool isLastOnTheLine(Position element) const {
        return (element.fGlyphRange.fEnd == SkToSizeT(fBoxLines.back().fBoxGlyphs.size()));
    }

    size_t countLines() const { return fBoxLines.size(); }
    BoxLine getLine(size_t lineIndex) const {
        SkASSERT(lineIndex < SkToSizeT(fBoxLines.size()));
        return fBoxLines[lineIndex];
    }

    bool hasProperty(TextIndex index, GlyphUnitFlags flag) const {
        return (fGlyphUnitProperties[index] & flag) == flag;
    }

    void onBeginLine(size_t index, TextRange lineText, bool hardBreak, SkRect bounds) override;
    void onEndLine(size_t index, TextRange lineText, GlyphRange trailingSpaces, size_t glyphCount) override;

    void onGlyphRun(const SkFont& font,
                    DirTextRange dirTextRange,
                    SkRect bounds,
                    TextIndex trailingSpaces,
                    size_t glyphCount,
                    const uint16_t glyphs[],
                    const SkPoint positions[],
                    const TextIndex clusters[]) override;

private:
    friend class WrappedText;

    Position findPosition(PositionType positionType, const BoxLine& line, SkScalar x) const;
    // Just in theory a random glyph range can be represented by multiple text ranges (because of LTR/RTL)
    // Currently we only support this method for a glyph, grapheme or grapheme cluster
    // So it's guaranteed to be one text range
    TextRange glyphsToText(Position position) const;

    SkTArray<BoxLine, true> fBoxLines;
    SkTArray<GlyphUnitFlags, true> fGlyphUnitProperties;
    SkSize fActualSize;
};

}  // namespace text
}  // namespace skia

#endif  // Text_DEFINED
