/*
 * Copyright (C) 2020 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.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;

/**
 * Information about the surrounding text around the cursor for use by an input method.
 *
 * <p>This contains information about the text and the selection relative to the text. </p>
 */
public final class SurroundingText implements Parcelable {
    /**
     * The surrounding text around the cursor.
     */
    @NonNull
    private final CharSequence mText;

    /**
     * The text offset of the start of the selection in the surrounding text.
     *
     * <p>This needs to be the position relative to the {@link #mText} instead of the real position
     * in the editor.</p>
     */
    @IntRange(from = 0)
    private final int mSelectionStart;

    /**
     * The text offset of the end of the selection in the surrounding text.
     *
     * <p>This needs to be the position relative to the {@link #mText} instead of the real position
     * in the editor.</p>
     */
    @IntRange(from = 0)
    private final int mSelectionEnd;

    /**
     * The text offset between the start of the editor's text and the start of the surrounding text.
     *
     * <p>-1 indicates the offset information is unknown.</p>
     */
    @IntRange(from = -1)
    private final int mOffset;

    /**
     * Constructor.
     *
     * @param text The surrounding text.
     * @param selectionStart The text offset of the start of the selection in the surrounding text.
     *                       Reversed selection is allowed.
     * @param selectionEnd The text offset of the end of the selection in the surrounding text.
     *                     Reversed selection is allowed.
     * @param offset The text offset between the start of the editor's text and the start of the
     *               surrounding text. -1 indicates the offset is unknown.
     */
    public SurroundingText(@NonNull final CharSequence text,
            @IntRange(from = 0) int selectionStart, @IntRange(from = 0) int selectionEnd,
            @IntRange(from = -1) int offset) {
        mText = copyWithParcelableSpans(text);
        mSelectionStart = selectionStart;
        mSelectionEnd = selectionEnd;
        mOffset = offset;
    }

    /**
     * Returns the surrounding text around the cursor.
     */
    @NonNull
    public CharSequence getText() {
        return mText;
    }

    /**
     * Returns the text offset of the start of the selection in the surrounding text.
     *
     * <p>A selection is the current range of the text that is selected by the user, or the current
     * position of the cursor. A cursor is a selection where the start and end are at the same
     * offset.<p>
     */
    @IntRange(from = 0)
    public int getSelectionStart() {
        return mSelectionStart;
    }

    /**
     * Returns the text offset of the end of the selection in the surrounding text.
     *
     * <p>A selection is the current range of the text that is selected by the user, or the current
     * position of the cursor. A cursor is a selection where the start and end are at the same
     * offset.<p>
     */
    @IntRange(from = 0)
    public int getSelectionEnd() {
        return mSelectionEnd;
    }

    /**
     * Returns text offset between the start of the editor's text and the start of the surrounding
     * text.
     *
     * <p>-1 indicates the offset information is unknown.</p>
     */
    @IntRange(from = -1)
    public int getOffset() {
        return mOffset;
    }

    @Override
    public void writeToParcel(@NonNull Parcel out, int flags) {
        TextUtils.writeToParcel(mText, out, flags);
        out.writeInt(mSelectionStart);
        out.writeInt(mSelectionEnd);
        out.writeInt(mOffset);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @NonNull
    public static final Parcelable.Creator<SurroundingText> CREATOR =
            new Parcelable.Creator<SurroundingText>() {
                @NonNull
                public SurroundingText createFromParcel(Parcel in) {
                    final CharSequence text =
                            TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
                    final int selectionHead = in.readInt();
                    final int selectionEnd = in.readInt();
                    final int offset = in.readInt();
                    return new SurroundingText(
                            text == null ? "" : text,  selectionHead, selectionEnd, offset);
                }

                @NonNull
                public SurroundingText[] newArray(int size) {
                    return new SurroundingText[size];
                }
            };

    /**
     * Create a copy of the given {@link CharSequence} object, with completely copy
     * {@link ParcelableSpan} instances.
     *
     * @param source the original {@link CharSequence} to be copied.
     * @return the copied {@link CharSequence}. {@code null} if {@code source} is {@code null}.
     */
    @Nullable
    private static CharSequence copyWithParcelableSpans(@Nullable CharSequence source) {
        if (source == null) {
            return null;
        }
        Parcel parcel = null;
        try {
            parcel = Parcel.obtain();
            TextUtils.writeToParcel(source, parcel, /* parcelableFlags= */ 0);
            parcel.setDataPosition(0);
            return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
        } finally {
            if (parcel != null) {
                parcel.recycle();
            }
        }
    }

    /** @hide */
    public boolean isEqualTo(@Nullable SurroundingText that) {
        if (that == null) return false;
        if (this == that) return true;
        return mSelectionStart == that.mSelectionStart
                && mSelectionEnd == that.mSelectionEnd
                && mOffset == that.mOffset
                && TextUtils.equals(mText, that.mText);
    }
}
