/*
 * Copyright (C) 2006 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.text;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.text.style.ParagraphStyle;

/**
 * A BoringLayout is a very simple Layout implementation for text that
 * fits on a single line and is all left-to-right characters.
 * You will probably never want to make one of these yourself;
 * if you do, be sure to call {@link #isBoring} first to make sure
 * the text meets the criteria.
 * <p>This class is used by widgets to control text layout. You should not need
 * to use this class directly unless you are implementing your own widget
 * or custom display object, in which case
 * you are encouraged to use a Layout instead of calling
 * {@link android.graphics.Canvas#drawText(java.lang.CharSequence, int, int, float, float, android.graphics.Paint)
 *  Canvas.drawText()} directly.</p>
 */
public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback {

    /**
     * Utility function to construct a BoringLayout instance.
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerWidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param spacingMult this value is no longer used by BoringLayout
     * @param spacingAdd this value is no longer used by BoringLayout
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     */
    public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,
            Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
            boolean includePad) {
        return new BoringLayout(source, paint, outerWidth, align, spacingMult, spacingAdd, metrics,
                includePad);
    }

    /**
     * Utility function to construct a BoringLayout instance.
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerWidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param spacingmult this value is no longer used by BoringLayout
     * @param spacingadd this value is no longer used by BoringLayout
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     * @param ellipsize whether to ellipsize the text if width of the text is longer than the
     *                  requested width
     * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
     *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
     *                        not used, {@code outerWidth} is used instead
     */
    public static BoringLayout make(CharSequence source, TextPaint paint, int outerWidth,
            Alignment align, float spacingmult, float spacingadd, BoringLayout.Metrics metrics,
            boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
        return new BoringLayout(source, paint, outerWidth, align, spacingmult, spacingadd, metrics,
                includePad, ellipsize, ellipsizedWidth);
    }

    /**
     * Utility function to construct a BoringLayout instance.
     *
     * The spacing multiplier and additional amount spacing are not used by BoringLayout.
     * {@link Layout#getSpacingMultiplier()} will return 1.0 and {@link Layout#getSpacingAdd()} will
     * return 0.0.
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerWidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     * @param ellipsize whether to ellipsize the text if width of the text is longer than the
     *                  requested width. null if ellipsis is not applied.
     * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
     *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
     *                        not used, {@code outerWidth} is used instead
     * @param useFallbackLineSpacing True for adjusting the line spacing based on fallback fonts.
     *                              False for keeping the first font's line height. If some glyphs
     *                              requires larger vertical spaces, by passing true to this
     *                              argument, the layout increase the line height to fit all glyphs.
     */
    public static @NonNull BoringLayout make(
            @NonNull CharSequence source, @NonNull TextPaint paint,
            @IntRange(from = 0) int outerWidth,
            @NonNull Alignment align, @NonNull BoringLayout.Metrics metrics,
            boolean includePad, @Nullable TextUtils.TruncateAt ellipsize,
            @IntRange(from = 0) int ellipsizedWidth, boolean useFallbackLineSpacing) {
        return new BoringLayout(source, paint, outerWidth, align, 1f, 0f, metrics, includePad,
                ellipsize, ellipsizedWidth, useFallbackLineSpacing);
    }

    /**
     * Returns a BoringLayout for the specified text, potentially reusing
     * this one if it is already suitable.  The caller must make sure that
     * no one is still using this Layout.
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerwidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param spacingMult this value is no longer used by BoringLayout
     * @param spacingAdd this value is no longer used by BoringLayout
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     */
    public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerwidth,
            Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
            boolean includePad) {
        replaceWith(source, paint, outerwidth, align, spacingMult, spacingAdd);

        mEllipsizedWidth = outerwidth;
        mEllipsizedStart = 0;
        mEllipsizedCount = 0;
        mUseFallbackLineSpacing = false;

        init(source, paint, align, metrics, includePad, true, false /* useFallbackLineSpacing */);
        return this;
    }

    /**
     * Returns a BoringLayout for the specified text, potentially reusing
     * this one if it is already suitable.  The caller must make sure that
     * no one is still using this Layout.
     *
     * The spacing multiplier and additional amount spacing are not used by BoringLayout.
     * {@link Layout#getSpacingMultiplier()} will return 1.0 and {@link Layout#getSpacingAdd()} will
     * return 0.0.
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerWidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     * @param ellipsize whether to ellipsize the text if width of the text is longer than the
     *                  requested width. null if ellipsis not applied.
     * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
     *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
     *                        not used, {@code outerWidth} is used instead
     * @param useFallbackLineSpacing True for adjusting the line spacing based on fallback fonts.
     *                              False for keeping the first font's line height. If some glyphs
     *                              requires larger vertical spaces, by passing true to this
     *                              argument, the layout increase the line height to fit all glyphs.
     */
    public @NonNull BoringLayout replaceOrMake(@NonNull CharSequence source,
            @NonNull TextPaint paint, @IntRange(from = 0) int outerWidth,
            @NonNull Alignment align, @NonNull BoringLayout.Metrics metrics, boolean includePad,
            @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth,
            boolean useFallbackLineSpacing) {
        boolean trust;

        if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
            replaceWith(source, paint, outerWidth, align, 1f, 0f);

            mEllipsizedWidth = outerWidth;
            mEllipsizedStart = 0;
            mEllipsizedCount = 0;
            trust = true;
        } else {
            replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),
                    paint, outerWidth, align, 1f, 0f);

            mEllipsizedWidth = ellipsizedWidth;
            trust = false;
        }

        mUseFallbackLineSpacing = useFallbackLineSpacing;

        init(getText(), paint, align, metrics, includePad, trust,
                useFallbackLineSpacing);
        return this;
    }

    /**
     * Returns a BoringLayout for the specified text, potentially reusing
     * this one if it is already suitable.  The caller must make sure that
     * no one is still using this Layout.
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerWidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param spacingMult this value is no longer used by BoringLayout
     * @param spacingAdd this value is no longer used by BoringLayout
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     * @param ellipsize whether to ellipsize the text if width of the text is longer than the
     *                  requested width
     * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
     *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
     *                        not used, {@code outerWidth} is used instead
     */
    public BoringLayout replaceOrMake(CharSequence source, TextPaint paint, int outerWidth,
            Alignment align, float spacingMult, float spacingAdd, BoringLayout.Metrics metrics,
            boolean includePad, TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
        return replaceOrMake(source, paint, outerWidth, align, metrics,
                includePad, ellipsize, ellipsizedWidth, false /* useFallbackLineSpacing */);
    }

    /**
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerwidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param spacingMult this value is no longer used by BoringLayout
     * @param spacingAdd this value is no longer used by BoringLayout
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     */
    public BoringLayout(CharSequence source, TextPaint paint, int outerwidth, Alignment align,
            float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad) {
        super(source, paint, outerwidth, align, spacingMult, spacingAdd);

        mEllipsizedWidth = outerwidth;
        mEllipsizedStart = 0;
        mEllipsizedCount = 0;
        mUseFallbackLineSpacing = false;

        init(source, paint, align, metrics, includePad, true, false /* useFallbackLineSpacing */);
    }

    /**
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerWidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param spacingMult this value is no longer used by BoringLayout
     * @param spacingAdd this value is no longer used by BoringLayout
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     * @param ellipsize whether to ellipsize the text if width of the text is longer than the
     *                  requested {@code outerWidth}
     * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
     *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
     *                        not used, {@code outerWidth} is used instead
     */
    public BoringLayout(CharSequence source, TextPaint paint, int outerWidth, Alignment align,
            float spacingMult, float spacingAdd, BoringLayout.Metrics metrics, boolean includePad,
            TextUtils.TruncateAt ellipsize, int ellipsizedWidth) {
        this(source, paint, outerWidth, align, spacingMult, spacingAdd, metrics, includePad,
                ellipsize, ellipsizedWidth, false /* fallbackLineSpacing */);
    }

    /**
     *
     * @param source the text to render
     * @param paint the default paint for the layout
     * @param outerWidth the wrapping width for the text
     * @param align whether to left, right, or center the text
     * @param spacingMult this value is no longer used by BoringLayout
     * @param spacingAdd this value is no longer used by BoringLayout
     * @param metrics {@code #Metrics} instance that contains information about FontMetrics and
     *                line width
     * @param includePad set whether to include extra space beyond font ascent and descent which is
     *                   needed to avoid clipping in some scripts
     * @param ellipsize whether to ellipsize the text if width of the text is longer than the
     *                  requested {@code outerWidth}. null if ellipsis is not applied.
     * @param ellipsizedWidth the width to which this Layout is ellipsizing. If {@code ellipsize} is
     *                        {@code null}, or is {@link TextUtils.TruncateAt#MARQUEE} this value is
     *                        not used, {@code outerWidth} is used instead
     * @param useFallbackLineSpacing True for adjusting the line spacing based on fallback fonts.
     *                              False for keeping the first font's line height. If some glyphs
     *                              requires larger vertical spaces, by passing true to this
     *                              argument, the layout increase the line height to fit all glyphs.
     */
    public BoringLayout(
            @NonNull CharSequence source, @NonNull TextPaint paint,
            @IntRange(from = 0) int outerWidth, @NonNull Alignment align, float spacingMult,
            float spacingAdd, @NonNull BoringLayout.Metrics metrics, boolean includePad,
            @Nullable TextUtils.TruncateAt ellipsize, @IntRange(from = 0) int ellipsizedWidth,
            boolean useFallbackLineSpacing) {
        /*
         * It is silly to have to call super() and then replaceWith(),
         * but we can't use "this" for the callback until the call to
         * super() finishes.
         */
        super(source, paint, outerWidth, align, spacingMult, spacingAdd);

        boolean trust;

        if (ellipsize == null || ellipsize == TextUtils.TruncateAt.MARQUEE) {
            mEllipsizedWidth = outerWidth;
            mEllipsizedStart = 0;
            mEllipsizedCount = 0;
            trust = true;
        } else {
            replaceWith(TextUtils.ellipsize(source, paint, ellipsizedWidth, ellipsize, true, this),
                        paint, outerWidth, align, spacingMult, spacingAdd);

            mEllipsizedWidth = ellipsizedWidth;
            trust = false;
        }

        mUseFallbackLineSpacing = useFallbackLineSpacing;
        init(getText(), paint, align, metrics, includePad, trust, useFallbackLineSpacing);
    }

    /* package */ void init(CharSequence source, TextPaint paint, Alignment align,
            BoringLayout.Metrics metrics, boolean includePad, boolean trustWidth,
            boolean useFallbackLineSpacing) {
        int spacing;

        if (source instanceof String && align == Layout.Alignment.ALIGN_NORMAL) {
            mDirect = source.toString();
        } else {
            mDirect = null;
        }

        mPaint = paint;

        if (includePad) {
            spacing = metrics.bottom - metrics.top;
            mDesc = metrics.bottom;
        } else {
            spacing = metrics.descent - metrics.ascent;
            mDesc = metrics.descent;
        }

        mBottom = spacing;

        if (trustWidth) {
            mMax = metrics.width;
        } else {
            /*
             * If we have ellipsized, we have to actually calculate the
             * width because the width that was passed in was for the
             * full text, not the ellipsized form.
             */
            TextLine line = TextLine.obtain();
            line.set(paint, source, 0, source.length(), Layout.DIR_LEFT_TO_RIGHT,
                    Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                    mEllipsizedStart, mEllipsizedStart + mEllipsizedCount, useFallbackLineSpacing);
            mMax = (int) Math.ceil(line.metrics(null));
            TextLine.recycle(line);
        }

        if (includePad) {
            mTopPadding = metrics.top - metrics.ascent;
            mBottomPadding = metrics.bottom - metrics.descent;
        }
    }

    /**
     * Determine and compute metrics if given text can be handled by BoringLayout.
     *
     * @param text a text
     * @param paint a paint
     * @return layout metric for the given text. null if given text is unable to be handled by
     *         BoringLayout.
     */
    public static Metrics isBoring(CharSequence text, TextPaint paint) {
        return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, null);
    }

    /**
     * Determine and compute metrics if given text can be handled by BoringLayout.
     *
     * @param text a text
     * @param paint a paint
     * @param metrics a metrics object to be recycled. If null is passed, this function creat new
     *                object.
     * @return layout metric for the given text. If metrics is not null, this method fills values
     *         to given metrics object instead of allocating new metrics object. null if given text
     *         is unable to be handled by BoringLayout.
     */
    public static Metrics isBoring(CharSequence text, TextPaint paint, Metrics metrics) {
        return isBoring(text, paint, TextDirectionHeuristics.FIRSTSTRONG_LTR, metrics);
    }

    /**
     * Returns true if the text contains any RTL characters, bidi format characters, or surrogate
     * code units.
     */
    private static boolean hasAnyInterestingChars(CharSequence text, int textLength) {
        final int MAX_BUF_LEN = 500;
        final char[] buffer = TextUtils.obtain(MAX_BUF_LEN);
        try {
            for (int start = 0; start < textLength; start += MAX_BUF_LEN) {
                final int end = Math.min(start + MAX_BUF_LEN, textLength);

                // No need to worry about getting half codepoints, since we consider surrogate code
                // units "interesting" as soon we see one.
                TextUtils.getChars(text, start, end, buffer, 0);

                final int len = end - start;
                for (int i = 0; i < len; i++) {
                    final char c = buffer[i];
                    if (c == '\n' || c == '\t' || TextUtils.couldAffectRtl(c)) {
                        return true;
                    }
                }
            }
            return false;
        } finally {
            TextUtils.recycle(buffer);
        }
    }

    /**
     * Returns null if not boring; the width, ascent, and descent in the
     * provided Metrics object (or a new one if the provided one was null)
     * if boring.
     * @hide
     */
    @UnsupportedAppUsage
    public static Metrics isBoring(CharSequence text, TextPaint paint,
            TextDirectionHeuristic textDir, Metrics metrics) {
        return isBoring(text, paint, textDir, false /* useFallbackLineSpacing */, metrics);
    }

    /**
     * Returns null if not boring; the width, ascent, and descent in the
     * provided Metrics object (or a new one if the provided one was null)
     * if boring.
     *
     * @param text a text to be calculated text layout.
     * @param paint a paint object used for styling.
     * @param textDir a text direction.
     * @param useFallbackLineSpacing True for adjusting the line spacing based on fallback fonts.
     *                              False for keeping the first font's line height. If some glyphs
     *                              requires larger vertical spaces, by passing true to this
     *                              argument, the layout increase the line height to fit all glyphs.
     * @param metrics the out metrics.
     * @return metrics on success. null if text cannot be rendered by BoringLayout.
     */
    public static @Nullable Metrics isBoring(@NonNull CharSequence text, @NonNull TextPaint paint,
            @NonNull TextDirectionHeuristic textDir, boolean useFallbackLineSpacing,
            @Nullable Metrics metrics) {
        final int textLength = text.length();
        if (hasAnyInterestingChars(text, textLength)) {
           return null;  // There are some interesting characters. Not boring.
        }
        if (textDir != null && textDir.isRtl(text, 0, textLength)) {
           return null;  // The heuristic considers the whole text RTL. Not boring.
        }
        if (text instanceof Spanned) {
            Spanned sp = (Spanned) text;
            Object[] styles = sp.getSpans(0, textLength, ParagraphStyle.class);
            if (styles.length > 0) {
                return null;  // There are some ParagraphStyle spans. Not boring.
            }
        }

        Metrics fm = metrics;
        if (fm == null) {
            fm = new Metrics();
        } else {
            fm.reset();
        }

        TextLine line = TextLine.obtain();
        line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
                Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null,
                0 /* ellipsisStart, 0 since text has not been ellipsized at this point */,
                0 /* ellipsisEnd, 0 since text has not been ellipsized at this point */,
                useFallbackLineSpacing);
        fm.width = (int) Math.ceil(line.metrics(fm));
        TextLine.recycle(line);

        return fm;
    }

    @Override
    public int getHeight() {
        return mBottom;
    }

    @Override
    public int getLineCount() {
        return 1;
    }

    @Override
    public int getLineTop(int line) {
        if (line == 0)
            return 0;
        else
            return mBottom;
    }

    @Override
    public int getLineDescent(int line) {
        return mDesc;
    }

    @Override
    public int getLineStart(int line) {
        if (line == 0)
            return 0;
        else
            return getText().length();
    }

    @Override
    public int getParagraphDirection(int line) {
        return DIR_LEFT_TO_RIGHT;
    }

    @Override
    public boolean getLineContainsTab(int line) {
        return false;
    }

    @Override
    public float getLineMax(int line) {
        return mMax;
    }

    @Override
    public float getLineWidth(int line) {
        return (line == 0 ? mMax : 0);
    }

    @Override
    public final Directions getLineDirections(int line) {
        return Layout.DIRS_ALL_LEFT_TO_RIGHT;
    }

    @Override
    public int getTopPadding() {
        return mTopPadding;
    }

    @Override
    public int getBottomPadding() {
        return mBottomPadding;
    }

    @Override
    public int getEllipsisCount(int line) {
        return mEllipsizedCount;
    }

    @Override
    public int getEllipsisStart(int line) {
        return mEllipsizedStart;
    }

    @Override
    public int getEllipsizedWidth() {
        return mEllipsizedWidth;
    }

    @Override
    public boolean isFallbackLineSpacingEnabled() {
        return mUseFallbackLineSpacing;
    }

    // Override draw so it will be faster.
    @Override
    public void draw(Canvas c, Path highlight, Paint highlightpaint,
                     int cursorOffset) {
        if (mDirect != null && highlight == null) {
            c.drawText(mDirect, 0, mBottom - mDesc, mPaint);
        } else {
            super.draw(c, highlight, highlightpaint, cursorOffset);
        }
    }

    /**
     * Callback for the ellipsizer to report what region it ellipsized.
     */
    public void ellipsized(int start, int end) {
        mEllipsizedStart = start;
        mEllipsizedCount = end - start;
    }

    private String mDirect;
    private Paint mPaint;
    private boolean mUseFallbackLineSpacing;

    /* package */ int mBottom, mDesc;   // for Direct
    private int mTopPadding, mBottomPadding;
    private float mMax;
    private int mEllipsizedWidth, mEllipsizedStart, mEllipsizedCount;

    public static class Metrics extends Paint.FontMetricsInt {
        public int width;

        @Override public String toString() {
            return super.toString() + " width=" + width;
        }

        private void reset() {
            top = 0;
            bottom = 0;
            ascent = 0;
            descent = 0;
            width = 0;
            leading = 0;
        }
    }
}
