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

import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.Px;

import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;

import libcore.util.NativeAllocationRegistry;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Provides automatic line breaking for a <em>single</em> paragraph.
 *
 * <p>
 * <pre>
 * <code>
 * Paint paint = new Paint();
 * Paint bigPaint = new Paint();
 * bigPaint.setTextSize(paint.getTextSize() * 2.0);
 * String text = "Hello, Android.";
 *
 * // Prepare the measured text
 * MeasuredText mt = new MeasuredText.Builder(text.toCharArray())
 *     .appendStyleRun(paint, 7, false)  // Use paint for "Hello, "
 *     .appednStyleRun(bigPaint, 8, false)  // Use bigPaint for "Hello, "
 *     .build();
 *
 * LineBreaker lb = new LineBreaker.Builder()
 *     // Use simple line breaker
 *     .setBreakStrategy(LineBreaker.BREAK_STRATEGY_SIMPLE)
 *     // Do not add hyphenation.
 *     .setHyphenationFrequency(LineBreaker.HYPHENATION_FREQUENCY_NONE)
 *     // Build the LineBreaker
 *     .build();
 *
 * ParagraphConstraints c = new ParagraphConstraints();
 * c.setWidth(240);  // Set the line wieth as 1024px
 *
 * // Do the line breaking
 * Result r = lb.computeLineBreaks(mt, c, 0);
 *
 * // Compute the total height of the text.
 * float totalHeight = 0;
 * for (int i = 0; i < r.getLineCount(); ++i) {  // iterate over the lines
 *    totalHeight += r.getLineDescent(i) - r.getLineAscent(i);
 * }
 *
 * // Draw text to the canvas
 * Bitmap bmp = new Bitmap.createBitmap(240, totalHeight, Bitmap.Config.ARGB_8888);
 * Canvas c = new Canvas(bmp);
 * float yOffset = 0f;
 * int prevOffset = 0;
 * for (int i = 0; i < r.getLineCount(); ++i) {  // iterate over the lines
 *     int nextOffset = r.getLineBreakOffset(i);
 *     c.drawText(text, prevOffset, nextOffset, 0f, yOffset, paint);
 *
 *     prevOffset = nextOffset;
 *     yOffset += r.getLineDescent(i) - r.getLineAscent(i);
 * }
 * </code>
 * </pre>
 * </p>
 */
public class LineBreaker {
    /** @hide */
    @IntDef(prefix = { "BREAK_STRATEGY_" }, value = {
            BREAK_STRATEGY_SIMPLE,
            BREAK_STRATEGY_HIGH_QUALITY,
            BREAK_STRATEGY_BALANCED
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface BreakStrategy {}

    /**
     * Value for break strategy indicating simple line breaking.
     *
     * The line breaker puts words to the line as much as possible and breaks line if no more words
     * can fit into the same line. Automatic hyphens are only added when a line has a single word
     * and that word is longer than line width. This is the fastest break strategy and ideal for
     * editor.
     */
    public static final int BREAK_STRATEGY_SIMPLE = 0;

    /**
     * Value for break strategy indicating high quality line breaking.
     *
     * With this option line breaker does whole-paragraph optimization for more readable text, and
     * also applies automatic hyphenation when required.
     */
    public static final int BREAK_STRATEGY_HIGH_QUALITY = 1;

    /**
     * Value for break strategy indicating balanced line breaking.
     *
     * The line breaker does whole-paragraph optimization for making all lines similar length, and
     * also applies automatic hyphenation when required. This break strategy is good for small
     * screen devices such as watch screens.
     */
    public static final int BREAK_STRATEGY_BALANCED = 2;

    /** @hide */
    @IntDef(prefix = { "HYPHENATION_FREQUENCY_" }, value = {
            HYPHENATION_FREQUENCY_NORMAL,
            HYPHENATION_FREQUENCY_FULL,
            HYPHENATION_FREQUENCY_NONE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface HyphenationFrequency {}

    /**
     * Value for hyphenation frequency indicating no automatic hyphenation.
     *
     * Using this option disables auto hyphenation which results in better text layout performance.
     * A word may be broken without hyphens when a line has a single word and that word is longer
     * than line width. Soft hyphens are ignored and will not be used as suggestions for potential
     * line breaks.
     */
    public static final int HYPHENATION_FREQUENCY_NONE = 0;

    /**
     * Value for hyphenation frequency indicating a light amount of automatic hyphenation.
     *
     * This hyphenation frequency is useful for informal cases, such as short sentences or chat
     * messages.
     */
    public static final int HYPHENATION_FREQUENCY_NORMAL = 1;

    /**
     * Value for hyphenation frequency indicating the full amount of automatic hyphenation.
     *
     * This hyphenation frequency is useful for running text and where it's important to put the
     * maximum amount of text in a screen with limited space.
     */
    public static final int HYPHENATION_FREQUENCY_FULL = 2;

    /** @hide */
    @IntDef(prefix = { "JUSTIFICATION_MODE_" }, value = {
            JUSTIFICATION_MODE_NONE,
            JUSTIFICATION_MODE_INTER_WORD
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface JustificationMode {}

    /**
     * Value for justification mode indicating no justification.
     */
    public static final int JUSTIFICATION_MODE_NONE = 0;

    /**
     * Value for justification mode indicating the text is justified by stretching word spacing.
     */
    public static final int JUSTIFICATION_MODE_INTER_WORD = 1;

    /**
     * Helper class for creating a {@link LineBreaker}.
     */
    public static class Builder {
        private @BreakStrategy int mBreakStrategy = BREAK_STRATEGY_SIMPLE;
        private @HyphenationFrequency int mHyphenationFrequency = HYPHENATION_FREQUENCY_NONE;
        private @JustificationMode int mJustified = JUSTIFICATION_MODE_NONE;
        private @Nullable int[] mIndents = null;

        /**
         * Set break strategy.
         *
         * You can change the line breaking behavior by setting break strategy. The default value is
         * {@link #BREAK_STRATEGY_SIMPLE}.
         */
        public Builder setBreakStrategy(@BreakStrategy int breakStrategy) {
            mBreakStrategy = breakStrategy;
            return this;
        }

        /**
         * Set hyphenation frequency.
         *
         * You can change the amount of automatic hyphenation used. The default value is
         * {@link #HYPHENATION_FREQUENCY_NONE}.
         */
        public Builder setHyphenationFrequency(@HyphenationFrequency int hyphenationFrequency) {
            mHyphenationFrequency = hyphenationFrequency;
            return this;
        }

        /**
         * Set whether the text is justified.
         *
         * By setting {@link #JUSTIFICATION_MODE_INTER_WORD}, the line breaker will change the
         * internal parameters for justification.
         * The default value is {@link #JUSTIFICATION_MODE_NONE}
         */
        public Builder setJustified(@JustificationMode int justified) {
            mJustified = justified;
            return this;
        }

        /**
         * Set indents.
         *
         * The supplied array provides the total amount of indentation per line, in pixel. This
         * amount is the sum of both left and right indentations. For lines past the last element in
         * the array, the indentation amount of the last element is used.
         */
        public Builder setIndents(@Nullable int[] indents) {
            mIndents = indents;
            return this;
        }

        /**
         * Build a new LineBreaker with given parameters.
         *
         * You can reuse the Builder instance even after calling this method.
         */
        public LineBreaker build() {
            return new LineBreaker(mBreakStrategy, mHyphenationFrequency, mJustified, mIndents);
        }
    }

    /**
     * Line breaking constraints for single paragraph.
     */
    public static class ParagraphConstraints {
        private @FloatRange(from = 0.0f) float mWidth = 0;
        private @FloatRange(from = 0.0f) float mFirstWidth = 0;
        private @IntRange(from = 0) int mFirstWidthLineCount = 0;
        private @Nullable int[] mVariableTabStops = null;
        private @IntRange(from = 0) int mDefaultTabStop = 0;

        public ParagraphConstraints() {}

        /**
         * Set width for this paragraph.
         *
         * @see #getWidth()
         */
        public void setWidth(@Px @FloatRange(from = 0.0f) float width) {
            mWidth = width;
        }

        /**
         * Set indent for this paragraph.
         *
         * @param firstWidth the line width of the starting of the paragraph
         * @param firstWidthLineCount the number of lines that applies the firstWidth
         * @see #getFirstWidth()
         * @see #getFirstWidthLineCount()
         */
        public void setIndent(@Px @FloatRange(from = 0.0f) float firstWidth,
                @Px @IntRange(from = 0) int firstWidthLineCount) {
            mFirstWidth = firstWidth;
            mFirstWidthLineCount = firstWidthLineCount;
        }

        /**
         * Set tab stops for this paragraph.
         *
         * @param tabStops the array of pixels of tap stopping position
         * @param defaultTabStop pixels of the default tab stopping position
         * @see #getTabStops()
         * @see #getDefaultTabStop()
         */
        public void setTabStops(@Nullable int[] tabStops,
                @Px @IntRange(from = 0) int defaultTabStop) {
            mVariableTabStops = tabStops;
            mDefaultTabStop = defaultTabStop;
        }

        /**
         * Return the width for this paragraph in pixels.
         *
         * @see #setWidth(float)
         */
        public @Px @FloatRange(from = 0.0f) float getWidth() {
            return mWidth;
        }

        /**
         * Return the first line's width for this paragraph in pixel.
         *
         * @see #setIndent(float, int)
         */
        public @Px @FloatRange(from = 0.0f) float getFirstWidth() {
            return mFirstWidth;
        }

        /**
         * Return the number of lines to apply the first line's width.
         *
         * @see #setIndent(float, int)
         */
        public @Px @IntRange(from = 0) int getFirstWidthLineCount() {
            return mFirstWidthLineCount;
        }

        /**
         * Returns the array of tab stops in pixels.
         *
         * @see #setTabStops(int[], int)
         */
        public @Nullable int[] getTabStops() {
            return mVariableTabStops;
        }

        /**
         * Returns the default tab stops in pixels.
         *
         * @see #setTabStop(int[], int)
         */
        public @Px @IntRange(from = 0) int getDefaultTabStop() {
            return mDefaultTabStop;
        }
    }

    /**
     * Holds the result of the {@link LineBreaker#computeLineBreaks line breaking algorithm}.
     * @see LineBreaker#computeLineBreaks
     */
    public static class Result {
        // Following two contstant must be synced with minikin's line breaker.
        private static final int TAB_MASK = 0x20000000;
        private static final int HYPHEN_MASK = 0xFF;

        private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
                Result.class.getClassLoader(), nGetReleaseResultFunc(), 32);
        private final long mPtr;

        private Result(long ptr) {
            mPtr = ptr;
            sRegistry.registerNativeAllocation(this, mPtr);
        }

        /**
         * Returns the number of lines in the paragraph.
         *
         * @return number of lines
         */
        public @IntRange(from = 0) int getLineCount() {
            return nGetLineCount(mPtr);
        }

        /**
         * Returns character offset of the break for a given line.
         *
         * @param lineIndex an index of the line.
         * @return the break offset.
         */
        public @IntRange(from = 0) int getLineBreakOffset(@IntRange(from = 0) int lineIndex) {
            return nGetLineBreakOffset(mPtr, lineIndex);
        }

        /**
         * Returns width of a given line in pixels.
         *
         * @param lineIndex an index of the line.
         * @return width of the line in pixels
         */
        public @Px float getLineWidth(@IntRange(from = 0) int lineIndex) {
            return nGetLineWidth(mPtr, lineIndex);
        }

        /**
         * Returns font ascent of the line in pixels.
         *
         * @param lineIndex an index of the line.
         * @return an entier font ascent of the line in pixels.
         */
        public @Px float getLineAscent(@IntRange(from = 0) int lineIndex) {
            return nGetLineAscent(mPtr, lineIndex);
        }

        /**
         * Returns font descent of the line in pixels.
         *
         * @param lineIndex an index of the line.
         * @return an entier font descent of the line in pixels.
         */
        public @Px float getLineDescent(@IntRange(from = 0) int lineIndex) {
            return nGetLineDescent(mPtr, lineIndex);
        }

        /**
         * Returns true if the line has a TAB character.
         *
         * @param lineIndex an index of the line.
         * @return true if the line has a TAB character
         */
        public boolean hasLineTab(int lineIndex) {
            return (nGetLineFlag(mPtr, lineIndex) & TAB_MASK) != 0;
        }

        /**
         * Returns a packed packed hyphen edit for the line.
         *
         * @param lineIndex an index of the line.
         * @return a packed hyphen edit for the line.
         *
         * @see android.text.Hyphenator#unpackStartHyphenEdit(int)
         * @see android.text.Hyphenator#unpackEndHyphenEdit(int)
         * @see android.text.Hyphenator#packHyphenEdit(int,int)
         */
        public int getLineHyphenEdit(int lineIndex) {
            return (nGetLineFlag(mPtr, lineIndex) & HYPHEN_MASK);
        }
    }

    private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
            LineBreaker.class.getClassLoader(), nGetReleaseFunc(), 64);

    private final long mNativePtr;

    /**
     * Use Builder instead.
     */
    private LineBreaker(@BreakStrategy int breakStrategy,
            @HyphenationFrequency int hyphenationFrequency, @JustificationMode int justify,
            @Nullable int[] indents) {
        mNativePtr = nInit(breakStrategy, hyphenationFrequency,
                justify == JUSTIFICATION_MODE_INTER_WORD, indents);
        sRegistry.registerNativeAllocation(this, mNativePtr);
    }

    /**
     * Break paragraph into lines.
     *
     * The result is filled to out param.
     *
     * @param measuredPara a result of the text measurement
     * @param constraints for a single paragraph
     * @param lineNumber a line number of this paragraph
     */
    public Result computeLineBreaks(
            @NonNull MeasuredText measuredPara,
            @NonNull ParagraphConstraints constraints,
            @IntRange(from = 0) int lineNumber) {
        return new Result(nComputeLineBreaks(
                mNativePtr,

                // Inputs
                measuredPara.getChars(),
                measuredPara.getNativePtr(),
                measuredPara.getChars().length,
                constraints.mFirstWidth,
                constraints.mFirstWidthLineCount,
                constraints.mWidth,
                constraints.mVariableTabStops,
                constraints.mDefaultTabStop,
                lineNumber));
    }

    @FastNative
    private static native long nInit(@BreakStrategy int breakStrategy,
            @HyphenationFrequency int hyphenationFrequency, boolean isJustified,
            @Nullable int[] indents);

    @CriticalNative
    private static native long nGetReleaseFunc();

    // populates LineBreaks and returns the number of breaks found
    //
    // the arrays inside the LineBreaks objects are passed in as well
    // to reduce the number of JNI calls in the common case where the
    // arrays do not have to be resized
    // The individual character widths will be returned in charWidths. The length of
    // charWidths must be at least the length of the text.
    private static native long nComputeLineBreaks(
            /* non zero */ long nativePtr,

            // Inputs
            @NonNull char[] text,
            /* Non Zero */ long measuredTextPtr,
            @IntRange(from = 0) int length,
            @FloatRange(from = 0.0f) float firstWidth,
            @IntRange(from = 0) int firstWidthLineCount,
            @FloatRange(from = 0.0f) float restWidth,
            @Nullable int[] variableTabStops,
            int defaultTabStop,
            @IntRange(from = 0) int indentsOffset);

    // Result accessors
    @CriticalNative
    private static native int nGetLineCount(long ptr);
    @CriticalNative
    private static native int nGetLineBreakOffset(long ptr, int idx);
    @CriticalNative
    private static native float nGetLineWidth(long ptr, int idx);
    @CriticalNative
    private static native float nGetLineAscent(long ptr, int idx);
    @CriticalNative
    private static native float nGetLineDescent(long ptr, int idx);
    @CriticalNative
    private static native int nGetLineFlag(long ptr, int idx);
    @CriticalNative
    private static native long nGetReleaseResultFunc();
}
