/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.view.inputmethod;

import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Layout;
import android.text.SegmentFinder;

import com.android.internal.util.ArrayUtils;
import com.android.internal.util.GrowingArrayUtils;
import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * The text bounds information of a slice of text in the editor.
 *
 * <p> This class provides IME the layout information of the text within the range from
 * {@link #getStartIndex()} to {@link #getEndIndex()}. It's intended to be used by IME as a
 * supplementary API to support handwriting gestures.
 * </p>
 */
public final class TextBoundsInfo implements Parcelable {
    /**
     * The flag indicating that the character is a whitespace.
     *
     * @see Builder#setCharacterFlags(int[])
     * @see #getCharacterFlags(int)
     */
    public static final int FLAG_CHARACTER_WHITESPACE = 1;

    /**
     * The flag indicating that the character is a linefeed character.
     *
     * @see Builder#setCharacterFlags(int[])
     * @see #getCharacterFlags(int)
     */
    public static final int FLAG_CHARACTER_LINEFEED = 1 << 1;

    /**
     * The flag indicating that the character is a punctuation.
     *
     * @see Builder#setCharacterFlags(int[])
     * @see #getCharacterFlags(int)
     */
    public static final int FLAG_CHARACTER_PUNCTUATION = 1 << 2;

    /**
     * The flag indicating that the line this character belongs to has RTL line direction. It's
     * required that all characters in the same line must have the same direction.
     *
     * @see Builder#setCharacterFlags(int[])
     * @see #getCharacterFlags(int)
     */
    public static final int FLAG_LINE_IS_RTL = 1 << 3;


    /** @hide */
    @IntDef(prefix = "FLAG_", flag = true, value = {
            FLAG_CHARACTER_WHITESPACE,
            FLAG_CHARACTER_LINEFEED,
            FLAG_CHARACTER_PUNCTUATION,
            FLAG_LINE_IS_RTL
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CharacterFlags {}

    /** All the valid flags. */
    private static final int KNOWN_CHARACTER_FLAGS = FLAG_CHARACTER_WHITESPACE
            | FLAG_CHARACTER_LINEFEED | FLAG_CHARACTER_PUNCTUATION  | FLAG_LINE_IS_RTL;

    /**
     * The amount of shift to get the character's BiDi level from the internal character flags.
     */
    private static final int BIDI_LEVEL_SHIFT = 19;

    /**
     * The mask used to get the character's BiDi level from the internal character flags.
     */
    private static final int BIDI_LEVEL_MASK = 0x7F << BIDI_LEVEL_SHIFT;

    /**
     * The flag indicating that the character at the index is the start of a line segment.
     * This flag is only used internally to serialize the {@link SegmentFinder}.
     *
     * @see #writeToParcel(Parcel, int)
     */
    private static final int FLAG_LINE_SEGMENT_START = 1 << 31;

    /**
     * The flag indicating that the character at the index is the end of a line segment.
     * This flag is only used internally to serialize the {@link SegmentFinder}.
     *
     * @see #writeToParcel(Parcel, int)
     */
    private static final int FLAG_LINE_SEGMENT_END = 1 << 30;

    /**
     * The flag indicating that the character at the index is the start of a word segment.
     * This flag is only used internally to serialize the {@link SegmentFinder}.
     *
     * @see #writeToParcel(Parcel, int)
     */
    private static final int FLAG_WORD_SEGMENT_START = 1 << 29;

    /**
     * The flag indicating that the character at the index is the end of a word segment.
     * This flag is only used internally to serialize the {@link SegmentFinder}.
     *
     * @see #writeToParcel(Parcel, int)
     */
    private static final int FLAG_WORD_SEGMENT_END = 1 << 28;

    /**
     * The flag indicating that the character at the index is the start of a grapheme segment.
     * It's only used internally to serialize the {@link SegmentFinder}.
     *
     * @see #writeToParcel(Parcel, int)
     */
    private static final int FLAG_GRAPHEME_SEGMENT_START = 1 << 27;

    /**
     * The flag indicating that the character at the index is the end of a grapheme segment.
     * It's only used internally to serialize the {@link SegmentFinder}.
     *
     * @see #writeToParcel(Parcel, int)
     */
    private static final int FLAG_GRAPHEME_SEGMENT_END = 1 << 26;

    private final int mStart;
    private final int mEnd;
    private final float[] mMatrixValues;
    private final float[] mCharacterBounds;
    /**
     * The array that encodes character and BiDi levels. They are stored together to save memory
     * space, and it's easier during serialization.
     */
    private final int[] mInternalCharacterFlags;
    private final SegmentFinder mLineSegmentFinder;
    private final SegmentFinder mWordSegmentFinder;
    private final SegmentFinder mGraphemeSegmentFinder;

    /**
     * Set the given {@link android.graphics.Matrix} to be the transformation
     * matrix that is to be applied other positional data in this class.
     */
    @NonNull
    public void getMatrix(@NonNull Matrix matrix) {
        Objects.requireNonNull(matrix);
        matrix.setValues(mMatrixValues);
    }

    /**
     * Returns the index of the first character whose bounds information is available in this
     * {@link TextBoundsInfo}, inclusive.
     *
     * @see Builder#setStartAndEnd(int, int)
     */
    public int getStartIndex() {
        return mStart;
    }

    /**
     * Returns the index of the last character whose bounds information is available in this
     * {@link TextBoundsInfo}, exclusive.
     *
     * @see Builder#setStartAndEnd(int, int)
     */
    public int getEndIndex() {
        return mEnd;
    }

    /**
     * Set the bounds of the character at the given {@code index} to the given {@link RectF}, in
     * the coordinates of the editor.
     *
     * @param index the index of the queried character.
     * @param bounds the {@link RectF} used to receive the result.
     *
     * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
     * the {@code start} to the {@code end}.
     */
    @NonNull
    public void getCharacterBounds(int index, @NonNull RectF bounds) {
        if (index < mStart || index >= mEnd) {
            throw new IndexOutOfBoundsException("Index is out of the bounds of "
                    + "[" + mStart + ", " + mEnd + ").");
        }
        final int offset = 4 * (index - mStart);
        bounds.set(mCharacterBounds[offset], mCharacterBounds[offset + 1],
                mCharacterBounds[offset + 2], mCharacterBounds[offset + 3]);
    }

    /**
     * Return the flags associated with the character at the given {@code index}.
     * The flags contain the following information:
     * <ul>
     *     <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
     *     whitespace. </li>
     *     <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
     *     linefeed. </li>
     *     <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
     *     punctuation. </li>
     *     <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
     *     has RTL line direction. All characters in the same line must have the same line
     *     direction. Check {@link #getLineSegmentFinder()} for more information of
     *     line boundaries. </li>
     * </ul>
     *
     * @param index the index of the queried character.
     * @return the flags associated with the queried character.
     *
     * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
     * the {@code start} to the {@code end}.
     *
     * @see #FLAG_CHARACTER_WHITESPACE
     * @see #FLAG_CHARACTER_LINEFEED
     * @see #FLAG_CHARACTER_PUNCTUATION
     * @see #FLAG_LINE_IS_RTL
     */
    @CharacterFlags
    public int getCharacterFlags(int index) {
        if (index < mStart || index >= mEnd) {
            throw new IndexOutOfBoundsException("Index is out of the bounds of "
                    + "[" + mStart + ", " + mEnd + ").");
        }
        final int offset = index - mStart;
        return mInternalCharacterFlags[offset] & KNOWN_CHARACTER_FLAGS;
    }

    /**
     * The BiDi level of the character at the given {@code index}. <br/>
     * BiDi level is defined by
     * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
     * bidirectional algorithm </a>. One can determine whether a character's direction is
     * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
     * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
     * character must be in the range of [0, 125].
     *
     * @param index the index of the queried character.
     * @return the BiDi level of the character, which is an integer in the range of [0, 125].
     * @throws IndexOutOfBoundsException if the given {@code index} is out of the range from
     * the {@code start} to the {@code end}.
     *
     * @see Builder#setCharacterBidiLevel(int[])
     */
    @IntRange(from = 0, to = 125)
    public int getCharacterBidiLevel(int index) {
        if (index < mStart || index >= mEnd) {
            throw new IndexOutOfBoundsException("Index is out of the bounds of "
                    + "[" + mStart + ", " + mEnd + ").");
        }
        final int offset = index - mStart;
        return (mInternalCharacterFlags[offset] & BIDI_LEVEL_MASK) >> BIDI_LEVEL_SHIFT;
    }

    /**
     * Returns the {@link SegmentFinder} that locates the word boundaries.
     *
     * @see Builder#setWordSegmentFinder(SegmentFinder)
     */
    @NonNull
    public SegmentFinder getWordSegmentFinder() {
        return mWordSegmentFinder;
    }

    /**
     * Returns the {@link SegmentFinder} that locates the grapheme boundaries.
     *
     * @see Builder#setGraphemeSegmentFinder(SegmentFinder)
     */
    @NonNull
    public SegmentFinder getGraphemeSegmentFinder() {
        return mGraphemeSegmentFinder;
    }

    /**
     * Returns the {@link SegmentFinder} that locates the line boundaries.
     *
     * @see Builder#setLineSegmentFinder(SegmentFinder)
     */
    @NonNull
    public SegmentFinder getLineSegmentFinder() {
        return mLineSegmentFinder;
    }

    /**
     * Return the index of the closest character to the given position.
     * It's similar to the text layout API {@link Layout#getOffsetForHorizontal(int, float)}.
     * And it's mainly used to find the cursor index (the index of the character before which the
     * cursor should be placed) for the given position. It's guaranteed that the returned index is
     * a grapheme break. Check {@link #getGraphemeSegmentFinder()} for more information.
     *
     * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom and each
     * line is laid out according to the display algorithm specified in
     * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
     * algorithm</a>.
     * </p>
     *
     * <p> This method won't check the text ranges whose line information is missing. For example,
     * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated
     * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
     * won't check the text in the ranges of [5, 7) and [12, 15).
     * </p>
     *
     * <p> Under the following conditions, this method will return -1 indicating that no valid
     * character is found:
     * <ul>
     *   <li> The given {@code y} coordinate is above the first line or below the last line (the
     *   first line or the last line is identified by the {@link SegmentFinder} returned from
     *   {@link #getLineSegmentFinder()}). </li>
     *   <li> There is no character in this {@link TextBoundsInfo}. </li>
     * </ul>
     * </p>
     *
     * @param x the x coordinates of the interested location, in the editor's coordinates.
     * @param y the y coordinates of the interested location, in the editor's coordinates.
     * @return the index of the character whose position is closest to the given location. It will
     * return -1 if it can't find a character.
     *
     * @see Layout#getOffsetForHorizontal(int, float)
     */
    public int getOffsetForPosition(float x, float y) {
        final int[] lineRange = new int[2];
        final RectF lineBounds = new RectF();
        getLineInfo(y, lineRange, lineBounds);
        // No line is found, return -1;
        if (lineRange[0] == -1 || lineRange[1] == -1) return -1;
        final int lineStart = lineRange[0];
        final int lineEnd = lineRange[1];

        final boolean lineEndsWithLinefeed =
                (getCharacterFlags(lineEnd - 1) & FLAG_CHARACTER_LINEFEED) != 0;

        // Consider the following 2 cases:
        // Case 1:
        //   Text: "AB\nCD"
        //   Layout: AB
        //           CD
        // Case 2:
        //   Text: "ABCD"
        //   Layout: AB
        //           CD
        // If user wants to insert a 'X' character at the end of the first line:
        //   In case 1, 'X' is inserted before the last character '\n'.
        //   In case 2, 'X' is inserted after the last character 'B'.
        // So if a line ends with linefeed, it shouldn't check the cursor position after the last
        // character.
        final int lineLimit;
        if (lineEndsWithLinefeed) {
            lineLimit = lineEnd;
        } else {
            lineLimit = lineEnd + 1;
        }
        // Point graphemeStart to the start of the first grapheme segment intersects with the line.
        int graphemeStart = mGraphemeSegmentFinder.nextEndBoundary(lineStart);
        // The grapheme information is missing.
        if (graphemeStart == SegmentFinder.DONE) return -1;
        graphemeStart = mGraphemeSegmentFinder.previousStartBoundary(graphemeStart);

        int target = -1;
        float minDistance = Float.MAX_VALUE;
        while (graphemeStart != SegmentFinder.DONE && graphemeStart < lineLimit) {
            if (graphemeStart >= lineStart) {
                float cursorPosition = getCursorHorizontalPosition(graphemeStart, lineStart,
                        lineEnd, lineBounds.left, lineBounds.right);
                final float distance = Math.abs(cursorPosition - x);
                if (distance < minDistance) {
                    minDistance = distance;
                    target = graphemeStart;
                }
            }
            graphemeStart = mGraphemeSegmentFinder.nextStartBoundary(graphemeStart);
        }

        return target;
    }

    /**
     * Whether the primary position at the given index is the previous character's trailing
     * position. <br/>
     *
     * For LTR character, trailing position is its right edge. For RTL character, trailing position
     * is its left edge.
     *
     * The primary position is defined as the position of a newly inserted character with the
     * context direction at the given offset. In contrast, the secondary position is the position
     * of a newly inserted character with the context's opposite direction at the given offset.
     *
     * In Android, the trailing position is used for primary position when the direction run after
     * the given index has a higher level than the current direction run.
     *
     * <p>
     * For example:
     * (L represents LTR character, and R represents RTL character. The number is the index)
     * <pre>
     * input text:          L0 L1 L2 R3 R4 R5 L6 L7 L8
     * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
     * BiDi Run:            [ Run 0 ][ Run 1 ][ Run 2 ]
     * BiDi Level:          0  0  0  1  1  1  0  0  0
     * </pre>
     *
     * The index 3 is a BiDi transition point, the cursor can be placed either after L2 or before
     * R3. Because the bidi level of run 1 is higher than the run 0, this method returns true. And
     * the cursor should be placed after L2.
     * <pre>
     * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
     * position after L2:           |
     * position before R3:                   |
     * result position:             |
     * </pre>
     *
     * The index 6 is also a Bidi transition point, the 2 possible cursor positions are exactly the
     * same as index 3. However, since the bidi level of run 2 is higher than the run 1, this
     * method returns false. And the cursor should be placed before L6.
     * <pre>
     * render result:       L0 L1 L2 R5 R4 R3 L6 L7 L8
     * position after R5:           |
     * position before L6:                   |
     * result position:                      |
     * </pre>
     *
     * This method helps guarantee that the cursor index and the cursor position forms a one to
     * one relation.
     * </p>
     *
     * @param offset the offset of the character in front of which the cursor is placed. It must be
     *              the start index of a grapheme. And it must be in the range from lineStart to
     *              lineEnd. An offset equal to lineEnd is allowed. It indicates that the cursor is
     *              placed at the end of current line instead of the start of the following line.
     * @param lineStart the start index of the line that index belongs to, inclusive.
     * @param lineEnd the end index of the line that index belongs to, exclusive.
     * @return true if primary position is the trailing position of the previous character.
     *
     * @see #getCursorHorizontalPosition(int, int, int, float, float)
     */
    private boolean primaryIsTrailingPrevious(int offset, int lineStart, int lineEnd) {
        final int bidiLevel;
        if (offset < lineEnd) {
            bidiLevel = getCharacterBidiLevel(offset);
        } else {
            // index equals to lineEnd, use line's BiDi level for the BiDi run.
            boolean lineIsRtl =
                    (getCharacterFlags(offset - 1) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
            bidiLevel = lineIsRtl ? 1 : 0;
        }
        final int bidiLevelBefore;
        if (offset > lineStart) {
            // Here it assumes index is always the start of a grapheme. And (index - 1) belongs to
            // the previous grapheme.
            bidiLevelBefore = getCharacterBidiLevel(offset - 1);
        } else {
            // index equals to lineStart, use line's BiDi level for previous BiDi run.
            boolean lineIsRtl =
                    (getCharacterFlags(offset) & FLAG_LINE_IS_RTL) == FLAG_LINE_IS_RTL;
            bidiLevelBefore = lineIsRtl ? 1 : 0;
        }
        return bidiLevelBefore < bidiLevel;
    }

    /**
     * Returns the x coordinates of the cursor at the given index. (The index of the character
     * before which the cursor should be placed.)
     *
     * @param index the character index before which the cursor is placed. It must be the start
     *              index of a grapheme. It must be in the range from lineStart to lineEnd.
     *              An index equal to lineEnd is allowed. It indicates that the cursor is
     *              placed at the end of current line instead of the start of the following line.
     * @param lineStart start index of the line that index belongs to, inclusive.
     * @param lineEnd end index of the line that index belongs, exclusive.
     * @return the x coordinates of the cursor at the given index,
     *
     * @see #primaryIsTrailingPrevious(int, int, int)
     */
    private float getCursorHorizontalPosition(int index, int lineStart, int lineEnd,
            float lineLeft, float lineRight) {
        Preconditions.checkArgumentInRange(index, lineStart, lineEnd, "index");
        final boolean lineIsRtl = (getCharacterFlags(lineStart) & FLAG_LINE_IS_RTL) != 0;
        final boolean isPrimaryIsTrailingPrevious =
                primaryIsTrailingPrevious(index, lineStart, lineEnd);

        // The index of the character used to compute the cursor position.
        final int targetIndex;
        // Whether to use the start position of the character.
        // For LTR character start is the left edge. For RTL character, start is the right edge.
        final boolean isStart;
        if (isPrimaryIsTrailingPrevious) {
            // (index - 1) belongs to the previous line(if any), return the line start position.
            if (index <= lineStart) {
                return lineIsRtl ? lineRight : lineLeft;
            }
            targetIndex = index - 1;
            isStart = false;
        } else {
            // index belongs to the next line(if any), return the line end position.
            if (index >= lineEnd) {
                return lineIsRtl ? lineLeft : lineRight;
            }
            targetIndex = index;
            isStart = true;
        }

        // The BiDi level is odd when the character is RTL.
        final boolean isRtl = (getCharacterBidiLevel(targetIndex) & 1) != 0;
        final int offset = targetIndex - mStart;
        // If the character is RTL, the start is the right edge. Otherwise, the start is the
        // left edge:
        //  +-----------------------+
        //  |       | start | end   |
        //  |-------+-------+-------|
        //  | RTL   | right | left  |
        //  |-------+-------+-------|
        //  | LTR   | left  | right |
        //  +-------+-------+-------+
        return (isRtl != isStart) ? mCharacterBounds[4 * offset] : mCharacterBounds[4 * offset + 2];
    }

    /**
     * Return the minimal rectangle that contains all the characters in the given range.
     *
     * @param start the start index of the given range, inclusive.
     * @param end the end index of the given range, exclusive.
     * @param rectF the {@link RectF} to receive the bounds.
     */
    private void getBoundsForRange(int start, int end, @NonNull RectF rectF) {
        Preconditions.checkArgumentInRange(start, mStart, mEnd - 1, "start");
        Preconditions.checkArgumentInRange(end, start, mEnd, "end");
        if (end <= start) {
            rectF.setEmpty();
            return;
        }

        rectF.left = Float.MAX_VALUE;
        rectF.top = Float.MAX_VALUE;
        rectF.right = Float.MIN_VALUE;
        rectF.bottom = Float.MIN_VALUE;
        for (int index = start; index < end; ++index) {
            final int offset = index - mStart;
            rectF.left = Math.min(rectF.left, mCharacterBounds[4 * offset]);
            rectF.top = Math.min(rectF.top, mCharacterBounds[4 * offset + 1]);
            rectF.right = Math.max(rectF.right, mCharacterBounds[4 * offset + 2]);
            rectF.bottom = Math.max(rectF.bottom, mCharacterBounds[4 * offset + 3]);
        }
    }

    /**
     * Return the character range and bounds of the closest line to the given {@code y} coordinate,
     * in the editor's local coordinates.
     *
     * If the given y is above the first line or below the last line -1 will be returned for line
     * start and end.
     *
     * This method assumes that the lines are laid out from the top to bottom.
     *
     * @param y the y coordinates used to search for the line.
     * @param characterRange a two element array used to receive the character range of the line.
     *                       If no valid line is found -1 will be returned for both start and end.
     * @param bounds {@link RectF} to receive the line bounds result, nullable. If given, it can
     *                            still be modified even if no valid line is found.
     */
    private void getLineInfo(float y, @NonNull int[] characterRange, @Nullable RectF bounds) {
        characterRange[0] = -1;
        characterRange[1] = -1;

        // Starting from the first line.
        int currentLineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
        if (currentLineEnd == SegmentFinder.DONE) return;
        int currentLineStart = mLineSegmentFinder.previousStartBoundary(currentLineEnd);

        float top = Float.MAX_VALUE;
        float bottom = Float.MIN_VALUE;
        float minDistance = Float.MAX_VALUE;
        final RectF currentLineBounds = new RectF();
        while (currentLineStart != SegmentFinder.DONE && currentLineStart < mEnd) {
            final int lineStartInRange = Math.max(mStart, currentLineStart);
            final int lineEndInRange = Math.min(mEnd, currentLineEnd);
            getBoundsForRange(lineStartInRange, lineEndInRange, currentLineBounds);

            top = Math.min(currentLineBounds.top, top);
            bottom = Math.max(currentLineBounds.bottom, bottom);

            final float distance = verticalDistance(currentLineBounds, y);

            if (distance == 0f) {
                characterRange[0] = currentLineStart;
                characterRange[1] = currentLineEnd;
                if (bounds != null) {
                    bounds.set(currentLineBounds);
                }
                return;
            }

            if (distance < minDistance) {
                minDistance = distance;
                characterRange[0] = currentLineStart;
                characterRange[1] = currentLineEnd;
                if (bounds != null) {
                    bounds.set(currentLineBounds);
                }
            }
            if (y < bounds.top) break;
            currentLineStart = mLineSegmentFinder.nextStartBoundary(currentLineStart);
            currentLineEnd = mLineSegmentFinder.nextEndBoundary(currentLineEnd);
        }

        // y is above the first line or below the last line. The founded line is still invalid,
        // clear the result.
        if (y < top || y > bottom) {
            characterRange[0] = -1;
            characterRange[1] = -1;
            if (bounds != null) {
                bounds.setEmpty();
            }
        }
    }

    /**
     * Finds the range of text which is inside the specified rectangle area. This method is a
     * counterpart of the
     * {@link Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)}.
     *
     * <p>It's assumed that the editor lays out text in horizontal lines from top to bottom
     * and each line is laid out according to the display algorithm specified in
     * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm"> unicode bidirectional
     * algorithm</a>.
     * </p>
     *
     * <p> This method won't check the text ranges whose line information is missing. For example,
     * the {@link TextBoundsInfo}'s range is from index 5 to 15. If the associated line
     * {@link SegmentFinder} only identifies one line range from 7 to 12. Then this method
     * won't check the text in the ranges of [5, 7) and [12, 15).
     * </p>
     *
     * @param area area for which the text range will be found
     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered as a
     *     text segment
     * @param inclusionStrategy strategy for determining whether a text segment is inside the
     *          specified area
     * @return the text range stored in a two element int array. The first element is the
     * start (inclusive) of the text range, and the second element is the end (exclusive) character
     * offsets of the text range, or null if there are no text segments inside the area.
     *
     * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)
     */
    @Nullable
    public int[] getRangeForRect(@NonNull RectF area, @NonNull SegmentFinder segmentFinder,
            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
        int lineEnd = mLineSegmentFinder.nextEndBoundary(mStart);
        // Line information is missing.
        if (lineEnd == SegmentFinder.DONE) return null;
        int lineStart = mLineSegmentFinder.previousStartBoundary(lineEnd);

        int start = -1;
        while (lineStart != SegmentFinder.DONE && start == -1) {
            start = getStartForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
                    inclusionStrategy);
            lineStart = mLineSegmentFinder.nextStartBoundary(lineStart);
            lineEnd = mLineSegmentFinder.nextEndBoundary(lineEnd);
        }

        // Can't find the start index; the specified contains no valid segment.
        if (start == -1) return null;

        lineStart = mLineSegmentFinder.previousStartBoundary(mEnd);
        // Line information is missing.
        if (lineStart == SegmentFinder.DONE) return null;
        lineEnd = mLineSegmentFinder.nextEndBoundary(lineStart);
        int end = -1;
        while (lineEnd > start && end == -1) {
            end = getEndForRectWithinLine(lineStart, lineEnd, area, segmentFinder,
                    inclusionStrategy);
            lineStart = mLineSegmentFinder.previousStartBoundary(lineStart);
            lineEnd = mLineSegmentFinder.previousEndBoundary(lineEnd);
        }

        // We've already found start, end is guaranteed to be found at this point.
        start = segmentFinder.previousStartBoundary(start + 1);
        end = segmentFinder.nextEndBoundary(end - 1);
        return new int[] { start, end };
    }

    /**
     * Find the start character index of the first text segments within a line inside the specified
     * {@code area}.
     *
     * @param lineStart the start of this line, inclusive .
     * @param lineEnd the end of this line, exclusive.
     * @param area the area inside which the text segments will be found.
     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
     *                      text segment.
     * @param inclusionStrategy strategy for determining whether a text segment is inside the
     *                          specified area.
     * @return the start index of the first segment in the area.
     */
    private int getStartForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
            @NonNull SegmentFinder segmentFinder,
            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
        if (lineStart >= lineEnd) return -1;

        int runStart = lineStart;
        int runLevel = -1;
        // Check the BiDi runs and search for the start index.
        for (int index = lineStart; index < lineEnd; ++index) {
            final int level = getCharacterBidiLevel(index);
            if (level != runLevel) {
                final int start = getStartForRectWithinRun(runStart, index, area, segmentFinder,
                        inclusionStrategy);
                if (start != -1) {
                    return start;
                }

                runStart = index;
                runLevel = level;
            }
        }
        return getStartForRectWithinRun(runStart, lineEnd, area, segmentFinder, inclusionStrategy);
    }

    /**
     * Find the start character index of the first text segments within the directional run inside
     * the specified {@code area}.
     *
     * @param runStart the start of this directional run, inclusive.
     * @param runEnd the end of this directional run, exclusive.
     * @param area the area inside which the text segments will be found.
     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
     *                      text segment.
     * @param inclusionStrategy strategy for determining whether a text segment is inside the
     *                          specified area.
     * @return the start index of the first segment in the area.
     */
    private int getStartForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
            @NonNull SegmentFinder segmentFinder,
            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
        if (runStart >= runEnd) return -1;

        int segmentEndOffset = segmentFinder.nextEndBoundary(runStart);
        // No segment is found in run.
        if (segmentEndOffset == SegmentFinder.DONE) return -1;
        int segmentStartOffset = segmentFinder.previousStartBoundary(segmentEndOffset);

        final RectF segmentBounds = new RectF();
        while (segmentStartOffset != SegmentFinder.DONE && segmentStartOffset < runEnd) {
            final int start = Math.max(runStart, segmentStartOffset);
            final int end = Math.min(runEnd, segmentEndOffset);
            getBoundsForRange(start, end, segmentBounds);
            // Find the first segment inside the area, return the start.
            if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return start;

            segmentStartOffset = segmentFinder.nextStartBoundary(segmentStartOffset);
            segmentEndOffset = segmentFinder.nextEndBoundary(segmentEndOffset);
        }
        return -1;
    }

    /**
     * Find the end character index of the last text segments within a line inside the specified
     * {@code area}.
     *
     * @param lineStart the start of this line, inclusive .
     * @param lineEnd the end of this line, exclusive.
     * @param area the area inside which the text segments will be found.
     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
     *                      text segment.
     * @param inclusionStrategy strategy for determining whether a text segment is inside the
     *                          specified area.
     * @return the end index of the last segment in the area.
     */
    private int getEndForRectWithinLine(int lineStart, int lineEnd, @NonNull RectF area,
            @NonNull SegmentFinder segmentFinder,
            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
        if (lineStart >= lineEnd) return -1;
        lineStart = Math.max(lineStart, mStart);
        lineEnd = Math.min(lineEnd, mEnd);

        // The exclusive run end index.
        int runEnd = lineEnd;
        int runLevel = -1;
        // Check the BiDi runs backwards and search for the end index.
        for (int index = lineEnd - 1; index >= lineStart; --index) {
            final int level = getCharacterBidiLevel(index);
            if (level != runLevel) {
                final int end = getEndForRectWithinRun(index + 1, runEnd, area, segmentFinder,
                        inclusionStrategy);
                if (end != -1) return end;

                runEnd = index + 1;
                runLevel = level;
            }
        }
        return getEndForRectWithinRun(lineStart, runEnd, area, segmentFinder, inclusionStrategy);
    }

    /**
     * Find the end character index of the last text segments within the directional run inside the
     * specified {@code area}.
     *
     * @param runStart the start of this directional run, inclusive.
     * @param runEnd the end of this directional run, exclusive.
     * @param area the area inside which the text segments will be found.
     * @param segmentFinder SegmentFinder for determining the ranges of text to be considered a
     *                      text segment.
     * @param inclusionStrategy strategy for determining whether a text segment is inside the
     *                          specified area.
     * @return the end index of the last segment in the area.
     */
    private int getEndForRectWithinRun(int runStart, int runEnd, @NonNull RectF area,
            @NonNull SegmentFinder segmentFinder,
            @NonNull Layout.TextInclusionStrategy inclusionStrategy) {
        if (runStart >= runEnd) return -1;

        int segmentStart = segmentFinder.previousStartBoundary(runEnd);
        // No segment is found before the runEnd.
        if (segmentStart == SegmentFinder.DONE) return -1;
        int segmentEnd = segmentFinder.nextEndBoundary(segmentStart);

        final RectF segmentBounds = new RectF();
        while (segmentEnd != SegmentFinder.DONE && segmentEnd > runStart) {
            final int start = Math.max(runStart, segmentStart);
            final int end = Math.min(runEnd, segmentEnd);
            getBoundsForRange(start, end, segmentBounds);
            // Find the last segment inside the area, return the end.
            if (inclusionStrategy.isSegmentInside(segmentBounds, area)) return end;

            segmentStart = segmentFinder.previousStartBoundary(segmentStart);
            segmentEnd = segmentFinder.previousEndBoundary(segmentEnd);
        }
        return -1;
    }

    /**
     * Get the vertical distance from the {@code pointF} to the {@code rectF}. It's useful to find
     * the corresponding line for a given point.
     */
    private static float verticalDistance(@NonNull RectF rectF, float y) {
        if (rectF.top <= y && y < rectF.bottom) {
            return 0f;
        }
        if (y < rectF.top) {
            return rectF.top - y;
        }
        return y - rectF.bottom;
    }

    /**
     * Describe the kinds of special objects contained in this Parcelable
     * instance's marshaled representation. For example, if the object will
     * include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
     * the return value of this method must include the
     * {@link #CONTENTS_FILE_DESCRIPTOR} bit.
     *
     * @return a bitmask indicating the set of special object types marshaled
     * by this Parcelable object instance.
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Flatten this object in to a Parcel.
     *
     * @param dest  The Parcel in which the object should be written.
     * @param flags Additional flags about how the object should be written.
     *              May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
     */
    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeInt(mStart);
        dest.writeInt(mEnd);
        dest.writeFloatArray(mMatrixValues);
        dest.writeFloatArray(mCharacterBounds);

        // The end can also be a break position. We need an extra space to encode the breaks.
        final int[] encodedFlags = Arrays.copyOf(mInternalCharacterFlags, mEnd - mStart + 1);
        encodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START, FLAG_GRAPHEME_SEGMENT_END,
                mStart, mEnd, mGraphemeSegmentFinder);
        encodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START, FLAG_WORD_SEGMENT_END, mStart,
                mEnd, mWordSegmentFinder);
        encodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START, FLAG_LINE_SEGMENT_END, mStart,
                mEnd, mLineSegmentFinder);
        dest.writeIntArray(encodedFlags);
    }

    private TextBoundsInfo(Parcel source) {
        mStart = source.readInt();
        mEnd  = source.readInt();
        mMatrixValues = Objects.requireNonNull(source.createFloatArray());
        mCharacterBounds = Objects.requireNonNull(source.createFloatArray());
        final int[] encodedFlags = Objects.requireNonNull(source.createIntArray());

        mGraphemeSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_GRAPHEME_SEGMENT_START,
                FLAG_GRAPHEME_SEGMENT_END, mStart, mEnd);
        mWordSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_WORD_SEGMENT_START,
                FLAG_WORD_SEGMENT_END, mStart, mEnd);
        mLineSegmentFinder = decodeSegmentFinder(encodedFlags, FLAG_LINE_SEGMENT_START,
                FLAG_LINE_SEGMENT_END, mStart, mEnd);

        final int length = mEnd - mStart;
        final int flagsMask = KNOWN_CHARACTER_FLAGS | BIDI_LEVEL_MASK;
        mInternalCharacterFlags = new int[length];
        for (int i = 0; i < length; ++i) {
            // Remove the flags used to encoded segment boundaries.
            mInternalCharacterFlags[i] = encodedFlags[i] & flagsMask;
        }
    }

    private TextBoundsInfo(Builder builder) {
        mStart = builder.mStart;
        mEnd = builder.mEnd;
        mMatrixValues = Arrays.copyOf(builder.mMatrixValues, 9);
        final int length = mEnd - mStart;
        mCharacterBounds = Arrays.copyOf(builder.mCharacterBounds, 4 * length);
        // Store characterFlags and characterBidiLevels to save memory.
        mInternalCharacterFlags = new int[length];
        for (int index = 0; index < length; ++index) {
            mInternalCharacterFlags[index] = builder.mCharacterFlags[index]
                    | (builder.mCharacterBidiLevels[index] << BIDI_LEVEL_SHIFT);
        }
        mGraphemeSegmentFinder = builder.mGraphemeSegmentFinder;
        mWordSegmentFinder = builder.mWordSegmentFinder;
        mLineSegmentFinder = builder.mLineSegmentFinder;
    }

    /**
     * The CREATOR to make this class Parcelable.
     */
    @NonNull
    public static final Parcelable.Creator<TextBoundsInfo> CREATOR = new Creator<TextBoundsInfo>() {
        @Override
        public TextBoundsInfo createFromParcel(Parcel source) {
            return new TextBoundsInfo(source);
        }

        @Override
        public TextBoundsInfo[] newArray(int size) {
            return new TextBoundsInfo[size];
        }
    };

    private static final String TEXT_BOUNDS_INFO_KEY = "android.view.inputmethod.TextBoundsInfo";

    /**
     * Store the {@link TextBoundsInfo} into a {@link Bundle}. This method is used by
     * {@link RemoteInputConnectionImpl} to transfer the {@link TextBoundsInfo} from the editor
     * to IME.
     *
     * @see TextBoundsInfoResult
     * @see InputConnection#requestTextBoundsInfo(RectF, Executor, Consumer)
     * @hide
     */
    @NonNull
    public Bundle toBundle() {
        final Bundle bundle = new Bundle();
        bundle.putParcelable(TEXT_BOUNDS_INFO_KEY, this);
        return bundle;

    }

    /** @hide */
    @Nullable
    public static TextBoundsInfo createFromBundle(@Nullable Bundle bundle) {
        if (bundle == null) return null;
        return bundle.getParcelable(TEXT_BOUNDS_INFO_KEY, TextBoundsInfo.class);
    }

    /**
     * The builder class to create a {@link TextBoundsInfo} object.
     */
    public static final class Builder {
        private final float[] mMatrixValues = new float[9];
        private boolean mMatrixInitialized;
        private int mStart = -1;
        private int mEnd = -1;
        private float[] mCharacterBounds;
        private int[] mCharacterFlags;
        private int[] mCharacterBidiLevels;
        private SegmentFinder mLineSegmentFinder;
        private SegmentFinder mWordSegmentFinder;
        private SegmentFinder mGraphemeSegmentFinder;

        /**
         * Create a builder for {@link TextBoundsInfo}.
         * @param start the start index of the {@link TextBoundsInfo}, inclusive.
         * @param end the end index of the {@link TextBoundsInfo}, exclusive.
         * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
         * or {@code end} is smaller than the {@code start}.
         */
        public Builder(int start, int end) {
            setStartAndEnd(start, end);
        }

        /** Clear all the parameters set on this {@link Builder} to reuse it. */
        @NonNull
        public Builder clear() {
            mMatrixInitialized = false;
            mStart = -1;
            mEnd = -1;
            mCharacterBounds = null;
            mCharacterFlags = null;
            mCharacterBidiLevels = null;
            mLineSegmentFinder = null;
            mWordSegmentFinder = null;
            mGraphemeSegmentFinder = null;
            return this;
        }

        /**
         * Sets the matrix that transforms local coordinates into screen coordinates.
         *
         * @param matrix transformation matrix from local coordinates into screen coordinates.
         * @throws NullPointerException if the given {@code matrix} is {@code null}.
         */
        @NonNull
        public Builder setMatrix(@NonNull Matrix matrix) {
            Objects.requireNonNull(matrix).getValues(mMatrixValues);
            mMatrixInitialized = true;
            return this;
        }

        /**
         * Set the start and end index of the {@link TextBoundsInfo}. It's the range of the
         * characters whose information is available in the {@link TextBoundsInfo}.
         *
         * @param start the start index of the {@link TextBoundsInfo}, inclusive.
         * @param end the end index of the {@link TextBoundsInfo}, exclusive.
         * @throws IllegalArgumentException if the given {@code start} or {@code end} is negative,
         * or {@code end} is smaller than the {@code start}.
         */
        @NonNull
        @SuppressWarnings("MissingGetterMatchingBuilder")
        public Builder setStartAndEnd(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
            Preconditions.checkArgument(start >= 0);
            Preconditions.checkArgumentInRange(start, 0, end, "start");
            mStart = start;
            mEnd = end;
            return this;
        }

        /**
         * Set the characters bounds, in the coordinates of the editor. <br/>
         *
         * The given array should be divided into groups of four where each element represents
         * left, top, right and bottom of the character bounds respectively.
         * The bounds of the i-th character in the editor should be stored at index
         * 4 * (i - start). The length of the given array must equal to 4 * (end - start). <br/>
         *
         * Sometimes multiple characters in a single grapheme are rendered as one symbol on the
         * screen. So those characters only have one shared bounds. In this case, we recommend the
         * editor to assign all the width to the bounds of the first character in the grapheme,
         * and make the rest characters' bounds zero-width. <br/>
         *
         * For example, the string "'0xD83D' '0xDE00'" is rendered as one grapheme - a grinning face
         * emoji. If the bounds of the grapheme is: Rect(5, 10, 15, 20), the character bounds of the
         * string should be: [ Rect(5, 10, 15, 20), Rect(15, 10, 15, 20) ].
         *
         * @param characterBounds the array of the flattened character bounds.
         * @throws NullPointerException if the given {@code characterBounds} is {@code null}.
         */
        @NonNull
        public Builder setCharacterBounds(@NonNull float[] characterBounds) {
            mCharacterBounds = Objects.requireNonNull(characterBounds);
            return this;
        }

        /**
         * Set the flags of the characters. The flags of the i-th character in the editor is stored
         * at index (i - start). The length of the given array must equal to (end - start).
         * The flags contain the following information:
         * <ul>
         *     <li>The {@link #FLAG_CHARACTER_WHITESPACE} flag, indicating the character is a
         *     whitespace. </li>
         *     <li>The {@link #FLAG_CHARACTER_LINEFEED} flag, indicating the character is a
         *     linefeed. </li>
         *     <li>The {@link #FLAG_CHARACTER_PUNCTUATION} flag, indicating the character is a
         *     punctuation. </li>
         *     <li>The {@link #FLAG_LINE_IS_RTL} flag, indicating the line this character belongs to
         *     is RTL. All all character in the same line must have the same line direction. Check
         *     {@link #getLineSegmentFinder()} for more information of line boundaries. </li>
         * </ul>
         *
         * @param characterFlags the array of the character's flags.
         * @throws NullPointerException if the given {@code characterFlags} is {@code null}.
         * @throws IllegalArgumentException if the given {@code characterFlags} contains invalid
         * flags.
         *
         * @see #getCharacterFlags(int)
         */
        @NonNull
        public Builder setCharacterFlags(@NonNull int[] characterFlags) {
            Objects.requireNonNull(characterFlags);
            for (int characterFlag : characterFlags) {
                if ((characterFlag & (~KNOWN_CHARACTER_FLAGS)) != 0) {
                    throw new IllegalArgumentException("characterFlags contains invalid flags.");
                }
            }
            mCharacterFlags = characterFlags;
            return this;
        }

        /**
         * Set the BiDi levels for the character. The bidiLevel of the i-th character in the editor
         * is stored at index (i - start). The length of the given array must equal to
         * (end - start). <br/>
         *
         * BiDi level is defined by
         * <a href="https://unicode.org/reports/tr9/#Basic_Display_Algorithm" >the unicode
         * bidirectional algorithm </a>. One can determine whether a character's direction is
         * right-to-left (RTL) or left-to-right (LTR) by checking the last bit of the BiDi level.
         * If it's 1, the character is RTL, otherwise the character is LTR. The BiDi level of a
         * character must be in the range of [0, 125].
         * @param characterBidiLevels the array of the character's BiDi level.
         *
         * @throws NullPointerException if the given {@code characterBidiLevels} is {@code null}.
         * @throws IllegalArgumentException if the given {@code characterBidiLevels} contains an
         * element that's out of the range [0, 125].
         *
         * @see #getCharacterBidiLevel(int)
         */
        @NonNull
        public Builder setCharacterBidiLevel(@NonNull int[] characterBidiLevels) {
            Objects.requireNonNull(characterBidiLevels);
            for (int index = 0; index < characterBidiLevels.length; ++index) {
                Preconditions.checkArgumentInRange(characterBidiLevels[index], 0, 125,
                        "bidiLevels[" + index + "]");
            }
            mCharacterBidiLevels = characterBidiLevels;
            return this;
        }

        /**
         * Set the {@link SegmentFinder} that locates the grapheme cluster boundaries. Grapheme is
         * defined in <a href="https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries">
         * the unicode annex #29: unicode text segmentation<a/>. It's a user-perspective character.
         * And it's usually the minimal unit for selection, backspace, deletion etc. <br/>
         *
         * Please note that only the grapheme segments within the range from start to end will
         * be available to the IME. The remaining information will be discarded during serialization
         * for better performance.
         *
         * @param graphemeSegmentFinder the {@link SegmentFinder} that locates the grapheme cluster
         *                              boundaries.
         * @throws NullPointerException if the given {@code graphemeSegmentFinder} is {@code null}.
         *
         * @see #getGraphemeSegmentFinder()
         * @see SegmentFinder
         * @see SegmentFinder.PrescribedSegmentFinder
         */
        @NonNull
        public Builder setGraphemeSegmentFinder(@NonNull SegmentFinder graphemeSegmentFinder) {
            mGraphemeSegmentFinder = Objects.requireNonNull(graphemeSegmentFinder);
            return this;
        }

        /**
         * Set the {@link SegmentFinder} that locates the word boundaries. <br/>
         *
         * Please note that only the word segments within the range from start to end will
         * be available to the IME. The remaining information will be discarded during serialization
         * for better performance.
         * @param wordSegmentFinder set the {@link SegmentFinder} that locates the word boundaries.
         * @throws NullPointerException if the given {@code wordSegmentFinder} is {@code null}.
         *
         * @see #getWordSegmentFinder()
         * @see SegmentFinder
         * @see SegmentFinder.PrescribedSegmentFinder
         */
        @NonNull
        public Builder setWordSegmentFinder(@NonNull SegmentFinder wordSegmentFinder) {
            mWordSegmentFinder = Objects.requireNonNull(wordSegmentFinder);
            return this;
        }

        /**
         * Set the {@link SegmentFinder} that locates the line boundaries. Aside from the hard
         * breaks in the text, it should also locate the soft line breaks added by the editor.
         * It is expected that the characters within the same line is rendered on the same baseline.
         * (Except for some text formatted as subscript and superscript.) <br/>
         *
         * Please note that only the line segments within the range from start to end will
         * be available to the IME. The remaining information will be discarded during serialization
         * for better performance.
         * @param lineSegmentFinder set the {@link SegmentFinder} that locates the line boundaries.
         * @throws NullPointerException if the given {@code lineSegmentFinder} is {@code null}.
         *
         * @see #getLineSegmentFinder()
         * @see SegmentFinder
         * @see SegmentFinder.PrescribedSegmentFinder
         */
        @NonNull
        public Builder setLineSegmentFinder(@NonNull SegmentFinder lineSegmentFinder) {
            mLineSegmentFinder = Objects.requireNonNull(lineSegmentFinder);
            return this;
        }

        /**
         * Create the {@link TextBoundsInfo} using the parameters in this {@link Builder}.
         *
         * @throws IllegalStateException in the following conditions:
         * <ul>
         *     <li>if the {@code start} or {@code end} is not set.</li>
         *     <li>if the {@code matrix} is not set.</li>
         *     <li>if {@code characterBounds} is not set or its length doesn't equal to
         *     4 * ({@code end} - {@code start}).</li>
         *     <li>if the {@code characterFlags} is not set or its length doesn't equal to
         *     ({@code end} - {@code start}).</li>
         *     <li>if {@code graphemeSegmentFinder}, {@code wordSegmentFinder} or
         *     {@code lineSegmentFinder} is not set.</li>
         *     <li>if characters in the same line has inconsistent {@link #FLAG_LINE_IS_RTL}
         *     flag.</li>
         * </ul>
         */
        @NonNull
        public TextBoundsInfo build() {
            if (mStart < 0 || mEnd < 0) {
                throw new IllegalStateException("Start and end must be set.");
            }

            if (!mMatrixInitialized) {
                throw new IllegalStateException("Matrix must be set.");
            }

            if (mCharacterBounds == null) {
                throw new IllegalStateException("CharacterBounds must be set.");
            }

            if (mCharacterFlags == null) {
                throw new IllegalStateException("CharacterFlags must be set.");
            }

            if (mCharacterBidiLevels == null) {
                throw new IllegalStateException("CharacterBidiLevel must be set.");
            }

            if (mCharacterBounds.length != 4 * (mEnd - mStart)) {
                throw new IllegalStateException("The length of characterBounds doesn't match the "
                        + "length of the given start and end."
                        + " Expected length: " + (4 * (mEnd - mStart))
                        + " characterBounds length: " + mCharacterBounds.length);
            }
            if (mCharacterFlags.length != mEnd - mStart) {
                throw new IllegalStateException("The length of characterFlags doesn't match the "
                        + "length of the given start and end."
                        + " Expected length: " + (mEnd - mStart)
                        + " characterFlags length: " + mCharacterFlags.length);
            }
            if (mCharacterBidiLevels.length != mEnd - mStart) {
                throw new IllegalStateException("The length of characterBidiLevels doesn't match"
                        + " the length of the given start and end."
                        + " Expected length: " + (mEnd - mStart)
                        + " characterFlags length: " + mCharacterBidiLevels.length);
            }
            if (mGraphemeSegmentFinder == null) {
                throw new IllegalStateException("GraphemeSegmentFinder must be set.");
            }
            if (mWordSegmentFinder == null) {
                throw new IllegalStateException("WordSegmentFinder must be set.");
            }
            if (mLineSegmentFinder == null) {
                throw new IllegalStateException("LineSegmentFinder must be set.");
            }

            if (!isLineDirectionFlagConsistent(mCharacterFlags, mLineSegmentFinder, mStart, mEnd)) {
                throw new IllegalStateException("characters in the same line must have the same "
                        + "FLAG_LINE_IS_RTL flag value.");
            }
            return new TextBoundsInfo(this);
        }
    }

    /**
     * Encode the segment start and end positions in {@link SegmentFinder} to a flags array.
     *
     * For example:
     * Text: "A BC DE"
     * Input:
     *     start: 2, end: 7                                     // substring "BC DE"
     *     SegmentFinder: segment ranges = [(2, 4), (5, 7)]     // a word break iterator
     *     flags: [0x0000, 0x0000, 0x0080, 0x0000, 0x0000, 0x0000] // 0x0080 is whitespace
     *     segmentStartFlag: 0x0100
     *     segmentEndFlag: 0x0200
     * Output:
     *     flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
     *  The index 2 and 5 encode segment starts, the index 4 and 7 encode a segment end.
     *
     * @param flags the flags array to receive the results.
     * @param segmentStartFlag the flag used to encode the segment start.
     * @param segmentEndFlag the flag used to encode the segment end.
     * @param start the start index of the encoded range, inclusive.
     * @param end the end index of the encoded range, inclusive.
     * @param segmentFinder the SegmentFinder to be encoded.
     *
     * @see #decodeSegmentFinder(int[], int, int, int, int)
     */
    private static void encodeSegmentFinder(@NonNull int[] flags, int segmentStartFlag,
            int segmentEndFlag, int start, int end, @NonNull SegmentFinder segmentFinder) {
        if (end - start + 1 != flags.length) {
            throw new IllegalStateException("The given flags array must have the same length as"
                    + " the given range. flags length: " + flags.length
                    + " range: [" + start + ", " + end + "]");
        }

        int segmentEnd = segmentFinder.nextEndBoundary(start);
        if (segmentEnd == SegmentFinder.DONE) return;
        int segmentStart = segmentFinder.previousStartBoundary(segmentEnd);

        while (segmentEnd != SegmentFinder.DONE && segmentEnd <= end) {
            if (segmentStart >= start) {
                flags[segmentStart - start] |= segmentStartFlag;
                flags[segmentEnd - start] |= segmentEndFlag;
            }
            segmentStart = segmentFinder.nextStartBoundary(segmentStart);
            segmentEnd = segmentFinder.nextEndBoundary(segmentEnd);
        }
    }

    /**
     * Decode a {@link SegmentFinder} from a flags array.
     *
     * For example:
     * Text: "A BC DE"
     * Input:
     *     start: 2, end: 7                                     // substring "BC DE"
     *     flags: [0x0100, 0x0000, 0x0280, 0x0100, 0x0000, 0x0200]
     *     segmentStartFlag: 0x0100
     *     segmentEndFlag: 0x0200
     * Output:
     *     SegmentFinder: segment ranges = [(2, 4), (5, 7)]
     *
     * @param flags the flags array to decode the SegmentFinder.
     * @param segmentStartFlag the flag to decode a segment start.
     * @param segmentEndFlag the flag to decode a segment end.
     * @param start the start index of the interested range, inclusive.
     * @param end the end index of the interested range, inclusive.
     *
     * @see #encodeSegmentFinder(int[], int, int, int, int, SegmentFinder)
     */
    private static SegmentFinder decodeSegmentFinder(int[] flags, int segmentStartFlag,
            int segmentEndFlag, int start, int end) {
        if (end - start + 1 != flags.length) {
            throw new IllegalStateException("The given flags array must have the same length as"
                    + " the given range. flags length: " + flags.length
                    + " range: [" + start + ", " + end + "]");
        }
        int[] breaks = ArrayUtils.newUnpaddedIntArray(10);
        int count = 0;
        for (int offset = 0; offset < flags.length; ++offset) {
            if ((flags[offset] & segmentStartFlag) == segmentStartFlag) {
                breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
            }
            if ((flags[offset] & segmentEndFlag) == segmentEndFlag) {
                breaks = GrowingArrayUtils.append(breaks, count++, start + offset);
            }
        }
        return new SegmentFinder.PrescribedSegmentFinder(Arrays.copyOf(breaks, count));
    }

    /**
     * Check whether the {@link #FLAG_LINE_IS_RTL} is the same for characters in the same line.
     * @return true if all characters in the same line has the same {@link #FLAG_LINE_IS_RTL} flag.
     */
    private static boolean isLineDirectionFlagConsistent(int[] characterFlags,
            SegmentFinder lineSegmentFinder, int start, int end) {
        int segmentEnd = lineSegmentFinder.nextEndBoundary(start);
        if (segmentEnd == SegmentFinder.DONE) return true;
        int segmentStart = lineSegmentFinder.previousStartBoundary(segmentEnd);

        while (segmentStart != SegmentFinder.DONE && segmentStart < end) {
            final int lineStart = Math.max(segmentStart, start);
            final int lineEnd = Math.min(segmentEnd, end);
            final boolean lineIsRtl = (characterFlags[lineStart - start] & FLAG_LINE_IS_RTL) != 0;
            for (int index = lineStart + 1; index < lineEnd; ++index) {
                final int flags = characterFlags[index - start];
                final boolean characterLineIsRtl = (flags & FLAG_LINE_IS_RTL) != 0;
                if (characterLineIsRtl != lineIsRtl) {
                    return false;
                }
            }

            segmentStart = lineSegmentFinder.nextStartBoundary(segmentStart);
            segmentEnd = lineSegmentFinder.nextEndBoundary(segmentEnd);
        }
        return true;
    }
}
