/*
 * 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.text;

import android.annotation.IntRange;
import android.graphics.RectF;

import androidx.annotation.NonNull;

import com.android.internal.util.Preconditions;

import java.util.Arrays;
import java.util.Objects;

/**
 * Finds text segment boundaries within text. Subclasses can implement different types of text
 * segments. Grapheme clusters and words are examples of possible text segments. These are
 * implemented by {@link GraphemeClusterSegmentFinder} and {@link WordSegmentFinder}.
 *
 * <p>Text segments may not overlap, so every character belongs to at most one text segment. A
 * character may belong to no text segments.
 *
 * <p>For example, WordSegmentFinder subdivides the text "Hello, World!" into four text segments:
 * "Hello", ",", "World", "!". The space character does not belong to any text segments.
 *
 * @see Layout#getRangeForRect(RectF, SegmentFinder, Layout.TextInclusionStrategy)
 */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public abstract class SegmentFinder {
    /**
     * Return value of previousStartBoundary(int), previousEndBoundary(int), nextStartBoundary(int),
     * and nextEndBoundary(int) when there are no boundaries of the specified type in the specified
     * direction.
     */
    public static final int DONE = -1;

    /**
     * Returns the character offset of the previous text segment start boundary before the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int previousStartBoundary(@IntRange(from = 0) int offset);

    /**
     * Returns the character offset of the previous text segment end boundary before the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int previousEndBoundary(@IntRange(from = 0) int offset);

    /**
     * Returns the character offset of the next text segment start boundary after the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int nextStartBoundary(@IntRange(from = 0) int offset);

    /**
     * Returns the character offset of the next text segment end boundary after the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int nextEndBoundary(@IntRange(from = 0) int offset);

    /**
     * The default {@link SegmentFinder} implementation based on given segment ranges.
     */
    public static class PrescribedSegmentFinder extends SegmentFinder {
        private final int[] mSegments;

        /**
         * Create a SegmentFinder with segments stored in an array, where i-th segment's start is
         * stored at segments[2 * i] and end is stored at segments[2 * i + 1] respectively.
         *
         * <p> It is required that segments do not overlap, and are already sorted by their start
         * indices. </p>
         * @param segments the array that stores the segment ranges.
         * @throws IllegalArgumentException if the given segments array's length is not even; the
         * given segments are not sorted or there are segments overlap with others.
         */
        public PrescribedSegmentFinder(@NonNull int[] segments) {
            checkSegmentsValid(segments);
            mSegments = segments;
        }

        /** {@inheritDoc} */
        @Override
        public int previousStartBoundary(@IntRange(from = 0) int offset) {
            return findPrevious(offset, /* isStart = */ true);
        }

        /** {@inheritDoc} */
        @Override
        public int previousEndBoundary(@IntRange(from = 0) int offset) {
            return findPrevious(offset, /* isStart = */ false);
        }

        /** {@inheritDoc} */
        @Override
        public int nextStartBoundary(@IntRange(from = 0) int offset) {
            return findNext(offset, /* isStart = */ true);
        }

        /** {@inheritDoc} */
        @Override
        public int nextEndBoundary(@IntRange(from = 0) int offset) {
            return findNext(offset, /* isStart = */ false);
        }

        private int findNext(int offset, boolean isStart) {
            if (offset < 0) return DONE;
            if (mSegments.length < 1 || offset > mSegments[mSegments.length - 1]) return DONE;

            if (offset < mSegments[0]) {
                return isStart ? mSegments[0] : mSegments[1];
            }

            int index = Arrays.binarySearch(mSegments, offset);
            if (index >= 0) {
                // mSegments may have duplicate elements (The previous segments end equals
                // to the following segments start.) Move the index forwards since we are searching
                // for the next segment.
                if (index + 1 < mSegments.length && mSegments[index + 1] == offset) {
                    index = index + 1;
                }
                // Point the index to the first segment boundary larger than the given offset.
                index += 1;
            } else {
                // binarySearch returns the insertion point, it's the first segment boundary larger
                // than the given offset.
                index = -(index + 1);
            }
            if (index >= mSegments.length) return DONE;

            //  +---------------------------------------+
            //  |               | isStart   | isEnd     |
            //  |---------------+-----------+-----------|
            //  | indexIsStart  | index     | index + 1 |
            //  |---------------+-----------+-----------|
            //  | indexIsEnd    | index + 1 | index     |
            //  +---------------------------------------+
            boolean indexIsStart = index % 2 == 0;
            if (isStart != indexIsStart) {
                return (index + 1 < mSegments.length) ? mSegments[index + 1] : DONE;
            }
            return mSegments[index];
        }

        private int findPrevious(int offset, boolean isStart) {
            if (mSegments.length < 1 || offset < mSegments[0]) return DONE;

            if (offset > mSegments[mSegments.length - 1]) {
                return isStart ? mSegments[mSegments.length - 2] : mSegments[mSegments.length - 1];
            }

            int index = Arrays.binarySearch(mSegments, offset);
            if (index >= 0) {
                // mSegments may have duplicate elements (when the previous segments end equal
                // to the following segments start). Move the index backwards since we are searching
                // for the previous segment.
                if (index > 0 && mSegments[index - 1] == offset) {
                    index = index - 1;
                }
                // Point the index to the first segment boundary smaller than the given offset.
                index -= 1;
            } else {
                // binarySearch returns the insertion point, insertionPoint - 1 is the first
                // segment boundary smaller than the given offset.
                index = -(index + 1) - 1;
            }
            if (index < 0) return DONE;

            //  +---------------------------------------+
            //  |               | isStart   | isEnd     |
            //  |---------------+-----------+-----------|
            //  | indexIsStart  | index     | index - 1 |
            //  |---------------+-----------+-----------|
            //  | indexIsEnd    | index - 1 | index     |
            //  +---------------------------------------+
            boolean indexIsStart = index % 2 == 0;
            if (isStart != indexIsStart) {
                return (index > 0) ? mSegments[index - 1] : DONE;
            }
            return mSegments[index];
        }

        private static void checkSegmentsValid(int[] segments) {
            Objects.requireNonNull(segments);
            Preconditions.checkArgument(segments.length % 2 == 0,
                    "the length of segments must be even");
            if (segments.length == 0) return;
            int lastSegmentEnd = Integer.MIN_VALUE;
            for (int index = 0; index < segments.length; index += 2) {
                if (segments[index] < lastSegmentEnd) {
                    throw new IllegalArgumentException("segments can't overlap");
                }
                if (segments[index] >= segments[index + 1]) {
                    throw new IllegalArgumentException("the segment range can't be empty");
                }
                lastSegmentEnd = segments[index + 1];
            }
        }
    }
}
