/*
* Copyright 2015 LinkedIn Corp. All rights reserved.
*
* 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.
*/

package com.linkedin.android.spyglass.ui;

import android.annotation.TargetApi;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.Editable;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.ArrowKeyMovementMethod;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.MenuRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.linkedin.android.spyglass.R;
import com.linkedin.android.spyglass.mentions.MentionSpan;
import com.linkedin.android.spyglass.mentions.MentionSpanConfig;
import com.linkedin.android.spyglass.mentions.Mentionable;
import com.linkedin.android.spyglass.mentions.MentionsEditable;
import com.linkedin.android.spyglass.suggestions.interfaces.SuggestionsVisibilityManager;
import com.linkedin.android.spyglass.tokenization.QueryToken;
import com.linkedin.android.spyglass.tokenization.interfaces.QueryTokenReceiver;
import com.linkedin.android.spyglass.tokenization.interfaces.TokenSource;
import com.linkedin.android.spyglass.tokenization.interfaces.Tokenizer;

import java.util.ArrayList;
import java.util.List;

/**
 * Class that overrides {@link EditText} in order to have more control over touch events and selection ranges for use in
 * the {@link RichEditorView}.
 * <p/>
 * <b>XML attributes</b>
 * <p/>
 * See {@link R.styleable#MentionsEditText Attributes}
 *
 * @attr ref R.styleable#MentionsEditText_mentionTextColor
 * @attr ref R.styleable#MentionsEditText_mentionTextBackgroundColor
 * @attr ref R.styleable#MentionsEditText_selectedMentionTextColor
 * @attr ref R.styleable#MentionsEditText_selectedMentionTextBackgroundColor
 */
public class MentionsEditText extends EditText implements TokenSource {

    private static final String KEY_MENTION_SPANS = "mention_spans";
    private static final String KEY_MENTION_SPAN_STARTS = "mention_span_starts";

    private Tokenizer mTokenizer;
    private QueryTokenReceiver mQueryTokenReceiver;
    private SuggestionsVisibilityManager mSuggestionsVisibilityManager;

    private List<MentionWatcher> mMentionWatchers = new ArrayList<>();
    private List<TextWatcher> mExternalTextWatchers = new ArrayList<>();
    private final MyWatcher mInternalTextWatcher = new MyWatcher();
    private boolean mBlockCompletion = false;
    private boolean mIsWatchingText = false;
    private boolean mAvoidPrefixOnTap = false;
    @Nullable private String mAvoidedPrefix;

    private MentionSpanFactory mentionSpanFactory;
    private MentionSpanConfig mentionSpanConfig;
    private boolean isLongPressed;
    private CheckLongClickRunnable longClickRunnable;

    public MentionsEditText(@NonNull Context context) {
        super(context);
        init(null, 0);
    }

    public MentionsEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public MentionsEditText(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    /**
     * Initialization method called by all constructors.
     */
    private void init(@Nullable AttributeSet attrs, int defStyleAttr) {
        // Get the mention span config from custom attributes
        mentionSpanConfig = parseMentionSpanConfigFromAttributes(attrs, defStyleAttr);

        // Must set movement method in order for MentionSpans to be clickable
        setMovementMethod(MentionsMovementMethod.getInstance());

        // Use MentionsEditable instead of default Editable
        setEditableFactory(MentionsEditableFactory.getInstance());

        // Start watching itself for text changes
        addTextChangedListener(mInternalTextWatcher);

        // Use default MentionSpanFactory initially
        mentionSpanFactory = new MentionSpanFactory();
    }

    private MentionSpanConfig parseMentionSpanConfigFromAttributes(@Nullable AttributeSet attrs, int defStyleAttr) {
        final Context context = getContext();
        MentionSpanConfig.Builder builder = new MentionSpanConfig.Builder();
        if (attrs == null) {
            return builder.build();
        }

        TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MentionsEditText, defStyleAttr, 0);
        @ColorInt int normalTextColor = attributes.getColor(R.styleable.MentionsEditText_mentionTextColor, -1);
        builder.setMentionTextColor(normalTextColor);
        @ColorInt int normalBgColor = attributes.getColor(R.styleable.MentionsEditText_mentionTextBackgroundColor, -1);
        builder.setMentionTextBackgroundColor(normalBgColor);
        @ColorInt int selectedTextColor = attributes.getColor(R.styleable.MentionsEditText_selectedMentionTextColor, -1);
        builder.setSelectedMentionTextColor(selectedTextColor);
        @ColorInt int selectedBgColor = attributes.getColor(R.styleable.MentionsEditText_selectedMentionTextBackgroundColor, -1);
        builder.setSelectedMentionTextBackgroundColor(selectedBgColor);

        attributes.recycle();

        return builder.build();
    }

    // --------------------------------------------------
    // TokenSource Interface Implementation
    // --------------------------------------------------

    /**
     * {@inheritDoc}
     */
    @Override
    @NonNull
    public String getCurrentTokenString() {
        // Get the text and ensure a valid tokenizer is set
        Editable text = getText();
        if (mTokenizer == null || text == null) {
            return "";
        }

        // Use current text to determine token string
        int cursor = Math.max(getSelectionStart(), 0);
        int start = mTokenizer.findTokenStart(text, cursor);
        int end = mTokenizer.findTokenEnd(text, cursor);
        String contentString = text.toString();
        return TextUtils.isEmpty(contentString) ? "" : contentString.substring(start, end);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Nullable
    public QueryToken getQueryTokenIfValid() {
        if (mTokenizer == null) {
            return null;
        }

        // Use current text to determine the start and end index of the token
        MentionsEditable text = getMentionsText();
        int cursor = Math.max(getSelectionStart(), 0);
        int start = mTokenizer.findTokenStart(text, cursor);
        int end = mTokenizer.findTokenEnd(text, cursor);

        if (!mTokenizer.isValidMention(text, start, end)) {
            return null;
        }

        String tokenString = text.subSequence(start, end).toString();
        char firstChar = tokenString.charAt(0);
        boolean isExplicit = mTokenizer.isExplicitChar(tokenString.charAt(0));
        return (isExplicit ? new QueryToken(tokenString, firstChar) : new QueryToken(tokenString));
    }

    // --------------------------------------------------
    // Touch Event Methods
    // --------------------------------------------------

    /**
     * Called whenever the user touches this {@link EditText}. This was one of the primary reasons for overriding
     * EditText in this library. This method ensures that when a user taps on a {@link MentionSpan} in this EditText,
     * {@link MentionSpan#onClick(View)} is called before the onClick method of this {@link EditText}.
     *
     * @param event the given {@link MotionEvent}
     *
     * @return true if the {@link MotionEvent} has been handled
     */
    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        final MentionSpan touchedSpan = getTouchedSpan(event);

        // Android 6 occasionally throws a NullPointerException inside Editor.onTouchEvent()
        // for ACTION_UP when attempting to display (uninitialised) text handles.
        boolean superResult;
        if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.M &&
            event.getActionMasked() == MotionEvent.ACTION_UP) {
            try {
                superResult = super.onTouchEvent(event);
            } catch (NullPointerException ignored) {
                // Ignore this (see above) - since we're now in an unknown state let's clear all
                // selection (which is still better than an arbitrary crash that we can't control):
                clearFocus();
                superResult = true;
            }
        } else {
            superResult = super.onTouchEvent(event);
        }

        if (event.getAction() == MotionEvent.ACTION_UP) {
            // Don't call the onclick on mention if MotionEvent.ACTION_UP is for long click action,
            if (!isLongPressed && touchedSpan != null) {
                // Manually click span and show soft keyboard
                touchedSpan.onClick(this);
                Context context = getContext();
                if (context != null) {
                    InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
                    imm.showSoftInput(this, 0);
                }
                return true;
            }
        } else if (event.getAction() == MotionEvent.ACTION_DOWN) {
            isLongPressed = false;
            if (isLongClickable() && touchedSpan != null) {
                if (longClickRunnable == null) {
                    longClickRunnable = new CheckLongClickRunnable();
                }
                longClickRunnable.touchedSpan = touchedSpan;
                removeCallbacks(longClickRunnable);
                postDelayed(longClickRunnable, ViewConfiguration.getLongPressTimeout());
            }
        } else if (event.getAction() == MotionEvent.ACTION_CANCEL) {
            isLongPressed = false;
        }

        // Check if user clicked on the EditText while showing the suggestions list
        // If so, avoid the current prefix
        if (mAvoidPrefixOnTap
                && mSuggestionsVisibilityManager != null
                && mSuggestionsVisibilityManager.isDisplayingSuggestions()) {
            mSuggestionsVisibilityManager.displaySuggestions(false);
            String keywords = getCurrentKeywordsString();
            String[] words = keywords.split(" ");
            if (words.length > 0) {
                String prefix = words[words.length - 1];
                // Note that prefix == "" when user types an explicit character and taps the EditText
                // We must not allow the user to avoid suggestions for the empty string prefix
                // Otherwise, explicit mentions would be broken, see MOB-38080
                if (prefix.length() > 0) {
                    setAvoidedPrefix(prefix);
                }
            }
        }
        return superResult;
    }

    @Override
    public boolean onTextContextMenuItem(@MenuRes int id) {
        MentionsEditable text = getMentionsText();
        int min = Math.max(0, getSelectionStart());
        int selectionEnd = getSelectionEnd();
        int max = selectionEnd >= 0 ? selectionEnd : text.length();
        // Ensuring that min is always less than or equal to max.
        min = Math.min(min, max);
        switch (id) {
            case android.R.id.cut:
                // First copy the span and then remove it from the current EditText
                copy(min, max);
                MentionSpan[] span = text.getSpans(min, max, MentionSpan.class);
                for (MentionSpan mentionSpan : span) {
                    text.removeSpan(mentionSpan);
                }
                text.delete(min, max);
                return true;
            case android.R.id.copy:
                copy(min, max);
                return true;
            case android.R.id.paste:
                paste(min, max);
                return true;
            default:
                return super.onTextContextMenuItem(id);
        }
    }

    /**
     * Copy the text between start and end in clipboard.
     * If no span is present, text is saved as plain text but if span is present
     * save it in Clipboard using intent.
     */
    private void copy(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
        MentionsEditable text = getMentionsText();
        SpannableStringBuilder copiedText = (SpannableStringBuilder) text.subSequence(start, end);
        MentionSpan[] spans = text.getSpans(start, end, MentionSpan.class);
        Intent intent = null;
        if (spans.length > 0) {
            // Save MentionSpan and it's start offset.
            intent = new Intent();
            intent.putExtra(KEY_MENTION_SPANS, spans);
            int[] spanStart = new int[spans.length];
            for (int i = 0; i < spans.length; i++) {
                spanStart[i] = copiedText.getSpanStart(spans[i]);
            }
            intent.putExtra(KEY_MENTION_SPAN_STARTS, spanStart);
        }
        saveToClipboard(copiedText, intent);
    }

    /**
     * Paste clipboard content between min and max positions.
     * If clipboard content contain the MentionSpan, set the span in copied text.
     */
    private void paste(@IntRange(from = 0) int min, @IntRange(from = 0) int max) {
        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
        ClipData clip = clipboard.getPrimaryClip();
        if (clip != null) {
            for (int i = 0; i < clip.getItemCount(); i++) {
                ClipData.Item item = clip.getItemAt(i);
                String selectedText = item.coerceToText(getContext()).toString();
                MentionsEditable text = getMentionsText();
                MentionSpan[] spans = text.getSpans(min, max, MentionSpan.class);
                /*
                 * We need to remove the span between min and max. This is required because in
                 * {@link SpannableStringBuilder#replace(int, int, CharSequence)} existing spans within
                 * the Editable that entirely cover the replaced range are retained, but any that
                 * were strictly within the range that was replaced are removed. In our case the existing
                 * spans are retained if the selection entirely covers the span. So, we just remove
                 * the existing span and replace the new text with that span.
                 */
                for (MentionSpan span : spans) {
                    if (text.getSpanEnd(span) == min) {
                        // We do not want to remove the span, when we want to paste anything just next
                        // to the existing span. In this case "text.getSpanEnd(span)" will be equal
                        // to min.
                        continue;
                    }
                    text.removeSpan(span);
                }

                Intent intent = item.getIntent();
                // Just set the plain text if we do not have mentions data in the intent/bundle
                if (intent == null) {
                    text.replace(min, max, selectedText);
                    continue;
                }
                Bundle bundle = intent.getExtras();
                if (bundle == null) {
                    text.replace(min, max, selectedText);
                    continue;
                }
                bundle.setClassLoader(getContext().getClassLoader());
                int[] spanStart = bundle.getIntArray(KEY_MENTION_SPAN_STARTS);
                Parcelable[] parcelables = bundle.getParcelableArray(KEY_MENTION_SPANS);
                if (parcelables == null || parcelables.length <= 0 || spanStart == null || spanStart.length <= 0) {
                    text.replace(min, max, selectedText);
                    continue;
                }

                // Set the MentionSpan in text.
                SpannableStringBuilder s = new SpannableStringBuilder(selectedText);
                for (int j = 0; j < parcelables.length; j++) {
                    MentionSpan mentionSpan = (MentionSpan) parcelables[j];
                    s.setSpan(mentionSpan, spanStart[j], spanStart[j] + mentionSpan.getDisplayString().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
                text.replace(min, max, s);
            }
        }
    }

    /**
     * Save the selected text and intent in ClipboardManager
     */
    private void saveToClipboard(@NonNull CharSequence selectedText, @Nullable Intent intent) {
        ClipboardManager clipboard = (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
        ClipData.Item item = new ClipData.Item(selectedText, intent, null);
        ClipData clip = new ClipData(null, new String[]{ClipDescription.MIMETYPE_TEXT_PLAIN}, item);
        clipboard.setPrimaryClip(clip);
    }

    /**
     * Gets the {@link MentionSpan} from the {@link MentionsEditText} that was tapped.
     * <p>
     * Note: Almost all of this code is taken directly from the Android source code, see:
     * {@link LinkMovementMethod#onTouchEvent(TextView, Spannable, MotionEvent)}
     *
     * @param event the given (@link MotionEvent}
     *
     * @return the tapped {@link MentionSpan}, or null if one was not tapped
     */
    @Nullable
    protected MentionSpan getTouchedSpan(@NonNull MotionEvent event) {
        Layout layout = getLayout();
        // Note: Layout can be null if text or width has recently changed, see MOB-38193
        if (event == null || layout == null) {
            return null;
        }

        int x = (int) event.getX();
        int y = (int) event.getY();

        x -= getTotalPaddingLeft();
        y -= getTotalPaddingTop();

        x += getScrollX();
        y += getScrollY();

        int line = layout.getLineForVertical(y);
        int off = layout.getOffsetForHorizontal(line, x);
        Editable text = getText();
        if (text != null && off >= getText().length()) {
            return null;
        }

        // Get the MentionSpans in the area that the user tapped
        // If one exists, call the onClick method manually
        MentionSpan[] spans = getText().getSpans(off, off, MentionSpan.class);
        if (spans.length > 0) {
            return spans[0];
        }
        return null;
    }

    // --------------------------------------------------
    // Cursor & Selection Event Methods
    // --------------------------------------------------

    /**
     * Called whenever the selection within the {@link EditText} has changed.
     *
     * @param selStart starting position of the selection
     * @param selEnd   ending position of the selection
     */
    @Override
    protected void onSelectionChanged(final int selStart, final int selEnd) {
        // Handle case where there is only one cursor (i.e. not selecting a range, just moving cursor)
        if (selStart == selEnd) {
            if (!onCursorChanged(selStart)) {
                super.onSelectionChanged(selStart, selEnd);
            }
            return;
        } else {
            updateSelectionIfRequired(selStart, selEnd);
        }
        super.onSelectionChanged(selStart, selEnd);
    }

    /**
     * Don't allow user to set starting position or ending position of selection within the mention.
     */
    private void updateSelectionIfRequired(final int selStart, final int selEnd) {
        MentionsEditable text = getMentionsText();
        MentionSpan startMentionSpan = text.getMentionSpanAtOffset(selStart);
        MentionSpan endMentionSpan = text.getMentionSpanAtOffset(selEnd);
        boolean selChanged = false;
        int start = selStart;
        int end = selEnd;
        if (text.getSpanStart(startMentionSpan) < selStart && selStart < text.getSpanEnd(startMentionSpan)) {
            start = text.getSpanStart(startMentionSpan);
            selChanged = true;
        }
        if (text.getSpanStart(endMentionSpan) < selEnd && selEnd < text.getSpanEnd(endMentionSpan)) {
            end = text.getSpanEnd(endMentionSpan);
            selChanged = true;
        }
        if (selChanged) {
            setSelection(start, end);
        }
    }

    /**
     * Method to handle the cursor changing positions. Returns true if handled, false if it should
     * be passed to the super method.
     *
     * @param index int position of cursor within the text
     *
     * @return true if handled
     */
    private boolean onCursorChanged(final int index) {
        Editable text = getText();
        if (text == null) {
            return false;
        }

        MentionSpan[] allSpans = text.getSpans(0, text.length(), MentionSpan.class);
        for (MentionSpan span : allSpans) {
            // Deselect span if the cursor is not on the span.
            if (span.isSelected() && (index < text.getSpanStart(span) || index > text.getSpanEnd(span))) {
                span.setSelected(false);
                updateSpan(span);
            }
        }

        // Do not allow the user to set the cursor within a span. If the user tries to do so, select
        // move the cursor to the end of it.
        MentionSpan[] currentSpans = text.getSpans(index, index, MentionSpan.class);
        if (currentSpans.length != 0) {
            MentionSpan span = currentSpans[0];
            int start = text.getSpanStart(span);
            int end = text.getSpanEnd(span);
            if (index > start && index < end) {
                super.setSelection(end);
                return true;
            }
        }

        return false;
    }

    // --------------------------------------------------
    // TextWatcher Implementation
    // --------------------------------------------------

    private class MyWatcher implements TextWatcher {

        /**
         * {@inheritDoc}
         */
        @Override
        public void beforeTextChanged(CharSequence text, int start, int before, int after) {
            if (mBlockCompletion) {
                return;
            }

            // Mark a span for deletion later if necessary
            boolean changed = markSpans(before, after);

            // If necessary, temporarily remove any MentionSpans that could potentially interfere with composing text
            if (!changed) {
                replaceMentionSpansWithPlaceholdersAsNecessary(text);
            }

            // Call any watchers for text changes
            sendBeforeTextChanged(text, start, before, after);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onTextChanged(CharSequence text, int start, int before, int count) {
            if (mBlockCompletion || !(text instanceof Editable) || getTokenizer() == null) {
                return;
            }

            // If the editor tries to insert duplicated text, mark the duplicated text for deletion later
            Editable editable = (Editable) text;
            int index = Selection.getSelectionStart(editable);
            Tokenizer tokenizer = getTokenizer();
            if (tokenizer != null) {
                markDuplicatedTextForDeletionLater((Editable) text, index, tokenizer);
            }

            // Call any watchers for text changes
            sendOnTextChanged(text, start, before, count);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void afterTextChanged(Editable text) {
            if (mBlockCompletion || text == null) {
                return;
            }

            // Block text change handling while we're changing the text (otherwise, may cause infinite loop)
            mBlockCompletion = true;

            // Text may have been marked to be removed in (before/on)TextChanged, remove that text now
            removeTextWithinDeleteSpans(text);

            // Some mentions may have been replaced by placeholders temporarily when altering the text, reinsert the
            // mention spans now
            replacePlaceholdersWithCorrespondingMentionSpans(text);

            // Ensure that the text in all the MentionSpans remains unchanged and valid
            ensureMentionSpanIntegrity(text);

            // Handle the change in text (can modify it freely here)
            handleTextChanged();

            // Allow class to listen for changes to the text again
            mBlockCompletion = false;

            // Call any watchers for text changes after we have handled it
            sendAfterTextChanged(text);
        }

        /**
         * Notify external text watchers that the text is about to change.
         * See {@link TextWatcher#beforeTextChanged(CharSequence, int, int, int)}.
         */
        private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
            final List<TextWatcher> list = mExternalTextWatchers;
            final int count = list.size();
            for (int i = 0; i < count; i++) {
                TextWatcher watcher = list.get(i);
                // Self check to avoid infinite loop
                if (watcher != null && watcher != this) {
                    watcher.beforeTextChanged(text, start, before, after);
                }
            }
        }

        /**
         * Notify external text watchers that the text is changing.
         * See {@link TextWatcher#onTextChanged(CharSequence, int, int, int)}.
         */
        private void sendOnTextChanged(CharSequence text, int start, int before, int after) {
            final List<TextWatcher> list = mExternalTextWatchers;
            final int count = list.size();
            for (int i = 0; i < count; i++) {
                TextWatcher watcher = list.get(i);
                // Self check to avoid infinite loop
                if (watcher != null && watcher != this) {
                    watcher.onTextChanged(text, start, before, after);
                }
            }
        }

        /**
         * Notify external text watchers that the text has changed.
         * See {@link TextWatcher#afterTextChanged(Editable)}.
         */
        private void sendAfterTextChanged(Editable text) {
            final List<TextWatcher> list = mExternalTextWatchers;
            final int count = list.size();
            for (int i = 0; i < count; i++) {
                TextWatcher watcher = list.get(i);
                // Self check to avoid infinite loop
                if (watcher != null && watcher != this) {
                    watcher.afterTextChanged(text);
                }
            }
        }
    }

    /**
     * Marks a span for deletion later if necessary by checking if the last character in a MentionSpan
     * is deleted by this change. If so, mark the span to be deleted later when
     * {@link #ensureMentionSpanIntegrity(Editable)} is called in {@link #handleTextChanged()}.
     *
     * @param count length of affected text before change starting at start in text
     * @param after length of affected text after change
     *
     * @return  true if there is a span before the cursor that is going to change state
     */
    private boolean markSpans(int count, int after) {
        int cursor = getSelectionStart();
        MentionsEditable text = getMentionsText();
        MentionSpan prevSpan = text.getMentionSpanEndingAt(cursor);
        boolean isNeedToMarkSpan = (count == (after + 1) || after == 0) && prevSpan != null;
        if (isNeedToMarkSpan) {

            // Cursor was directly behind a span and was moved back one, so delete it if selected,
            // or select it if not already selected
            if (prevSpan.isSelected()) {
                Mentionable mention = prevSpan.getMention();
                Mentionable.MentionDeleteStyle deleteStyle = mention.getDeleteStyle();
                Mentionable.MentionDisplayMode displayMode = prevSpan.getDisplayMode();
                // Determine new DisplayMode given previous DisplayMode and MentionDeleteStyle
                if (deleteStyle == Mentionable.MentionDeleteStyle.PARTIAL_NAME_DELETE
                        && displayMode == Mentionable.MentionDisplayMode.FULL) {
                    prevSpan.setDisplayMode(Mentionable.MentionDisplayMode.PARTIAL);
                } else {
                    prevSpan.setDisplayMode(Mentionable.MentionDisplayMode.NONE);
                }
            } else {
                // Span was not selected, so select it
                prevSpan.setSelected(true);
            }

            return true;
        }

        return false;
    }

    /**
     * Temporarily remove MentionSpans that may interfere with composing text. Note that software keyboards are allowed
     * to place arbitrary spans over the text. This was resulting in several bugs in edge cases while handling the
     * MentionSpans while composing text (with different issues for different keyboards). The easiest solution for this
     * is to remove any MentionSpans that could cause issues while the user is changing text.
     *
     * Note: The MentionSpans are added again in {@link #replacePlaceholdersWithCorrespondingMentionSpans(Editable)}
     *
     * @param text the current text before it changes
     */
    private void replaceMentionSpansWithPlaceholdersAsNecessary(@NonNull CharSequence text) {
        int index = getSelectionStart();
        int wordStart = findStartOfWord(text, index);
        Editable editable = getText();
        MentionSpan[] mentionSpansInCurrentWord = editable.getSpans(wordStart, index, MentionSpan.class);
        for (MentionSpan span : mentionSpansInCurrentWord) {
            if (span.getDisplayMode() != Mentionable.MentionDisplayMode.NONE) {
                int spanStart = editable.getSpanStart(span);
                int spanEnd = editable.getSpanEnd(span);
                editable.setSpan(new PlaceholderSpan(span, spanStart, spanEnd),
                        spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
                editable.removeSpan(span);
            }
        }
    }

    /**
     * Helper utility to determine the beginning of a word using the current tokenizer.
     *
     * @param text  the text to examine
     * @param index index of the cursor in the text
     * @return  index of the beginning of the word in text (will be less than or equal to index)
     */
    private int findStartOfWord(@NonNull CharSequence text, int index) {
        int wordStart = index;
        while (wordStart > 0 && mTokenizer != null && !mTokenizer.isWordBreakingChar(text.charAt(wordStart - 1))) {
            wordStart--;
        }
        return wordStart;
    }

    /**
     * Mark text that was duplicated during text composition to delete it later.
     *
     * @param text          the given text
     * @param cursor        the index of the cursor in text
     * @param tokenizer     the {@link Tokenizer} to use
     */
    private void markDuplicatedTextForDeletionLater(@NonNull Editable text, int cursor, @NonNull Tokenizer tokenizer) {
        while (cursor > 0 && tokenizer.isWordBreakingChar(text.charAt(cursor - 1))) {
            cursor--;
        }
        int wordStart = findStartOfWord(text, cursor);
        PlaceholderSpan[] placeholderSpans = text.getSpans(wordStart, wordStart + 1, PlaceholderSpan.class);
        for (PlaceholderSpan span : placeholderSpans) {
            int spanEnd = span.originalEnd;
            int copyEnd = spanEnd + (spanEnd - wordStart);
            if (copyEnd > spanEnd && copyEnd <= text.length()) {
                CharSequence endOfMention = text.subSequence(wordStart, spanEnd);
                CharSequence copyOfEndOfMentionText = text.subSequence(spanEnd, copyEnd);
                // Note: Comparing strings since we do not want to compare any other aspects of spanned strings
                if (endOfMention.toString().equals(copyOfEndOfMentionText.toString())) {
                    text.setSpan(new DeleteSpan(),
                            spanEnd,
                            copyEnd,
                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
        }
    }

    /**
     * Removes any {@link com.linkedin.android.spyglass.ui.MentionsEditText.DeleteSpan}s and the text within them from
     * the given text.
     *
     * @param text the editable containing DeleteSpans to remove
     */
    private void removeTextWithinDeleteSpans(@NonNull Editable text) {
        DeleteSpan[] deleteSpans = text.getSpans(0, text.length(), DeleteSpan.class);
        for (DeleteSpan span : deleteSpans) {
            int spanStart = text.getSpanStart(span);
            int spanEnd = text.getSpanEnd(span);
            text.replace(spanStart, spanEnd, "");
            text.removeSpan(span);
        }
    }

    /**
     * Replaces any {@link com.linkedin.android.spyglass.ui.MentionsEditText.PlaceholderSpan} within the given text with
     * the {@link MentionSpan} it contains.
     *
     * Note: These PlaceholderSpans are added in {@link #replaceMentionSpansWithPlaceholdersAsNecessary(CharSequence)}
     *
     * @param text the final version of the text after it was changed
     */
    private void replacePlaceholdersWithCorrespondingMentionSpans(@NonNull Editable text) {
        PlaceholderSpan[] tempSpans = text.getSpans(0, text.length(), PlaceholderSpan.class);
        for (PlaceholderSpan span : tempSpans) {
            int spanStart = text.getSpanStart(span);
            String mentionDisplayString = span.holder.getDisplayString();
            int end = Math.min(spanStart + mentionDisplayString.length(), text.length());
            text.replace(spanStart, end, mentionDisplayString);
            text.setSpan(span.holder, spanStart, spanStart + mentionDisplayString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            text.removeSpan(span);
        }
    }

    /**
     * Ensures that the text within each {@link MentionSpan} in the {@link Editable} correctly
     * matches what it should be outputting. If not, replace it with the correct value.
     *
     * @param text the {@link Editable} to examine
     */
    private void ensureMentionSpanIntegrity(Editable text) {
        if (text == null) {
            return;
        }

        MentionSpan[] spans = text.getSpans(0, text.length(), MentionSpan.class);
        boolean spanAltered = false;
        for (MentionSpan span : spans) {
            int start = text.getSpanStart(span);
            int end = text.getSpanEnd(span);
            CharSequence spanText = text.subSequence(start, end).toString();
            Mentionable.MentionDisplayMode displayMode = span.getDisplayMode();

            switch (displayMode) {

                case PARTIAL:
                case FULL:
                    String name = span.getDisplayString();
                    if (!name.contentEquals(spanText) && start >= 0 && start < end && end <= text.length()) {
                        // Mention display name does not match what is being shown,
                        // replace text in span with proper display name
                        int cursor = getSelectionStart();
                        int diff = cursor - end;
                        text.removeSpan(span);
                        text.replace(start, end, name);
                        if (diff > 0 && start + end + diff < text.length()) {
                            text.replace(start + end, start + end + diff, "");
                        }
                        if (name.length() > 0) {
                            text.setSpan(span, start, start + name.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                        }
                        // Notify for partially deleted mentions.
                        if (mMentionWatchers.size() > 0 && displayMode == Mentionable.MentionDisplayMode.PARTIAL) {
                            notifyMentionPartiallyDeletedWatchers(span.getMention(), name, start, end);
                        }
                        spanAltered = true;
                    }
                    break;

                case NONE:
                default:
                    // Mention with DisplayMode == NONE should be deleted from the text
                    boolean hasListeners = mMentionWatchers.size() > 0;
                    final String textBeforeDelete = hasListeners ? text.toString() : null;
                    text.delete(start, end);
                    setSelection(start);
                    if (hasListeners) {
                        notifyMentionDeletedWatchers(span.getMention(), textBeforeDelete, start, end);
                    }
                    spanAltered = true;
                    break;
            }
        }

        // Reset input method if spans have been changed (updates suggestions)
        if (spanAltered) {
            restartInput();
        }
    }

    /**
     * Called after the {@link Editable} text within the {@link EditText} has been changed. Note that
     * editing text in this function is guaranteed to be safe and not cause an infinite loop.
     */
    private void handleTextChanged() {
        // Ignore requests if the last word in keywords is prefixed by the currently avoided prefix
        if (mAvoidedPrefix != null) {
            String[] keywords = getCurrentKeywordsString().split(" ");
            // Add null and length check to avoid the ArrayIndexOutOfBoundsException
            if (keywords.length == 0) {
                return;
            }
            String lastKeyword = keywords[keywords.length - 1];
            if (lastKeyword.startsWith(mAvoidedPrefix)) {
                return;
            } else {
                setAvoidedPrefix(null);
            }
        }

        // Request suggestions from the QueryClient
        QueryToken queryToken = getQueryTokenIfValid();
        if (queryToken != null && mQueryTokenReceiver != null) {
            // Valid token, so send query to the app for processing
            mQueryTokenReceiver.onQueryReceived(queryToken);
        } else {
            // Ensure that the suggestions are hidden
            if (mSuggestionsVisibilityManager != null) {
                mSuggestionsVisibilityManager.displaySuggestions(false);
            }
        }
    }

    // --------------------------------------------------
    // Public Methods
    // --------------------------------------------------

    /**
     * Gets the keywords that the {@link Tokenizer} is currently considering for mention suggestions. Note that this is
     * the keywords string and will not include any explicit character, if present.
     *
     * @return a String representing current keywords in the {@link EditText}
     */
    @NonNull
    public String getCurrentKeywordsString() {
        String keywordsString = getCurrentTokenString();
        if (keywordsString.length() > 0 && mTokenizer.isExplicitChar(keywordsString.charAt(0))) {
            keywordsString = keywordsString.substring(1);
        }
        return keywordsString;
    }

    /**
     * Resets the given {@link MentionSpan} in the editor, forcing it to redraw with its latest drawable state.
     *
     * @param span the {@link MentionSpan} to update
     */
    public void updateSpan(@NonNull MentionSpan span) {
        mBlockCompletion = true;
        Editable text = getText();
        int start = text.getSpanStart(span);
        int end = text.getSpanEnd(span);
        if (start >= 0 && end > start && end <= text.length()) {
            text.removeSpan(span);
            text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
        mBlockCompletion = false;
    }

    /**
     * Deselects any spans in the editor that are currently selected.
     */
    public void deselectAllSpans() {
        mBlockCompletion = true;
        Editable text = getText();
        MentionSpan[] spans = text.getSpans(0, text.length(), MentionSpan.class);
        for (MentionSpan span : spans) {
            if (span.isSelected()) {
                span.setSelected(false);
                updateSpan(span);
            }
        }
        mBlockCompletion = false;
    }

    /**
     * Inserts a mention into the token being considered currently.
     *
     * @param mention {@link Mentionable} to insert a span for
     */
    public void insertMention(@NonNull Mentionable mention) {
        if (mTokenizer == null) {
            return;
        }

        // Setup variables and ensure they are valid
        Editable text = getEditableText();
        int cursor = getSelectionStart();
        int start = mTokenizer.findTokenStart(text, cursor);
        int end = mTokenizer.findTokenEnd(text, cursor);
        if (start < 0 || start >= end || end > text.length()) {
            return;
        }

        insertMentionInternal(mention, text, start, end);
    }

    /**
     * Inserts a mention. This will not take any token into consideration. This method is useful
     * when you want to insert a mention which doesn't have a token.
     *
     * @param mention {@link Mentionable} to insert a span for
     */
    public void insertMentionWithoutToken(@NonNull Mentionable mention) {
        // Setup variables and ensure they are valid
        Editable text = getEditableText();
        int index = getSelectionStart();
        index = index > 0 ? index : 0;

        insertMentionInternal(mention, text, index, index);
    }

    private void insertMentionInternal(@NonNull Mentionable mention, @NonNull Editable text, int start, int end) {
        // Insert the span into the editor
        MentionSpan mentionSpan = mentionSpanFactory.createMentionSpan(mention, mentionSpanConfig);
        String name = mention.getSuggestiblePrimaryText();

        mBlockCompletion = true;
        text.replace(start, end, name);
        int endOfMention = start + name.length();
        text.setSpan(mentionSpan, start, endOfMention, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        Selection.setSelection(text, endOfMention);
        ensureMentionSpanIntegrity(text);
        mBlockCompletion = false;

        // Notify listeners of added mention
        if (mMentionWatchers.size() > 0) {
            notifyMentionAddedWatchers(mention, text.toString(), start, endOfMention);
        }

        // Hide the suggestions and clear adapter
        if (mSuggestionsVisibilityManager != null) {
            mSuggestionsVisibilityManager.displaySuggestions(false);
        }

        // Reset input method since text has been changed (updates mention draw states)
        restartInput();
    }

    /**
     * Determines if the {@link Tokenizer} is looking at an explicit token right now.
     *
     * @return true if the {@link Tokenizer} is currently considering an explicit query
     */
    public boolean isCurrentlyExplicit() {
        String tokenString = getCurrentTokenString();
        return tokenString.length() > 0 && mTokenizer != null && mTokenizer.isExplicitChar(tokenString.charAt(0));
    }

    /**
     * Populate an {@link AccessibilityEvent} with information about this text view. Note that this implementation uses
     * a copy of the text that is explicitly not an instance of {@link MentionsEditable}. This is due to the fact that
     * AccessibilityEvent will use the default system classloader when unparcelling the data within the event. This
     * results in a ClassNotFoundException. For more details, see: https://github.com/linkedin/Spyglass/issues/10
     *
     * @param event the populated AccessibilityEvent
     */
    @Override
    public void onPopulateAccessibilityEvent(@NonNull AccessibilityEvent event) {
        super.onPopulateAccessibilityEvent(event);
        List<CharSequence> textList = event.getText();
        CharSequence mentionLessText = getTextWithoutMentions();
        for (int i = 0; i < textList.size(); i++) {
            CharSequence text = textList.get(i);
            if (text instanceof MentionsEditable) {
                textList.set(i, mentionLessText);
            }
        }
    }

    /**
     * Allows a class to watch for text changes. Note that adding this class to itself will add it
     * to the super class. Other instances of {@link TextWatcher} will be notified by this class
     * as appropriate (helps prevent infinite loops when text keeps changing).
     *
     * @param watcher the {@link TextWatcher} to add
     */
    @Override
    public void addTextChangedListener(@NonNull TextWatcher watcher) {
        if (watcher == mInternalTextWatcher) {
            if (!mIsWatchingText) {
                super.addTextChangedListener(mInternalTextWatcher);
                mIsWatchingText = true;
            }
        } else {
            mExternalTextWatchers.add(watcher);
        }
    }

    /**
     * Removes a {@link TextWatcher} from this class. Note that this function servers as the
     * counterpart to {@link #addTextChangedListener(TextWatcher)}).
     *
     * @param watcher the {@link TextWatcher} to remove
     */
    @Override
    public void removeTextChangedListener(@NonNull TextWatcher watcher) {
        if (watcher == mInternalTextWatcher) {
            if (mIsWatchingText) {
                super.removeTextChangedListener(mInternalTextWatcher);
                mIsWatchingText = false;
            }
        } else {
            // Other watchers are added
            mExternalTextWatchers.remove(watcher);
        }
    }

    /**
     * Register a {@link com.linkedin.android.spyglass.ui.MentionsEditText.MentionWatcher} in order to receive callbacks
     * when mentions are changed.
     *
     * @param watcher the {@link com.linkedin.android.spyglass.ui.MentionsEditText.MentionWatcher} to add
     */
    @SuppressWarnings("unused")
    public void addMentionWatcher(@NonNull MentionWatcher watcher) {
        if (!mMentionWatchers.contains(watcher)) {
            mMentionWatchers.add(watcher);
        }
    }

    /**
     * Remove a {@link com.linkedin.android.spyglass.ui.MentionsEditText.MentionWatcher} from receiving anymore callbacks
     * when mentions are changed.
     *
     * @param watcher the {@link com.linkedin.android.spyglass.ui.MentionsEditText.MentionWatcher} to remove
     */
    @SuppressWarnings("unused")
    public void removeMentionWatcher(@NonNull MentionWatcher watcher) {
        mMentionWatchers.remove(watcher);
    }

    // --------------------------------------------------
    // Private Helper Methods
    // --------------------------------------------------

    private void restartInput() {
        InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
        if (imm != null) {
            imm.restartInput(this);
        }
    }

    /**
     * @return the text as an {@link Editable} (note: not a {@link MentionsEditable} and does not contain mentions)
     */
    private Editable getTextWithoutMentions() {
        Editable text = getText();
        SpannableStringBuilder sb = new SpannableStringBuilder(text);
        MentionSpan[] spans = sb.getSpans(0, sb.length(), MentionSpan.class);
        for (MentionSpan span: spans) {
            sb.removeSpan(span);
        }
        return sb;
    }

    private void notifyMentionAddedWatchers(@NonNull Mentionable mention, @NonNull String text, int start, int end) {
        for (MentionWatcher watcher : mMentionWatchers) {
            watcher.onMentionAdded(mention, text, start, end);
        }
    }

    private void notifyMentionDeletedWatchers(@NonNull Mentionable mention, @NonNull String text, int start, int end) {
        for (MentionWatcher watcher : mMentionWatchers) {
            watcher.onMentionDeleted(mention, text, start, end);
        }
    }

    private void notifyMentionPartiallyDeletedWatchers(@NonNull Mentionable mention, @NonNull String text, int start, int end) {
        for (MentionWatcher watcher : mMentionWatchers) {
            watcher.onMentionPartiallyDeleted(mention, text, start, end);
        }
    }

    // --------------------------------------------------
    // Private Classes
    // --------------------------------------------------

    /**
     * Simple class to hold onto a {@link MentionSpan} temporarily while the text is changing.
     */
    private class PlaceholderSpan {

        final MentionSpan holder;
        final int originalStart;
        final int originalEnd;

        PlaceholderSpan(MentionSpan holder, int originalStart, int originalEnd) {
            this.holder = holder;
            this.originalStart = originalStart;
            this.originalEnd = originalEnd;
        }
    }

    /**
     * Simple class to mark a span of text to delete later.
     */
    private class DeleteSpan {}

    /**
     * Runnable which detects the long click action.
     */
    private class CheckLongClickRunnable implements Runnable {
        private MentionSpan touchedSpan;

        @Override
        public void run() {
            if (isPressed()) {
                isLongPressed = true;
                if (touchedSpan == null) {
                    return;
                }
                MentionsEditable text = getMentionsText();
                // Set the selection anchor to start and end of the long clicked span and deselect all the span.
                setSelection(text.getSpanStart(touchedSpan), text.getSpanEnd(touchedSpan));
                deselectAllSpans();
            }
        }
    }

    // --------------------------------------------------
    // MentionsEditable Factory
    // --------------------------------------------------

    /**
     * Custom EditableFactory designed so that we can use the customized {@link MentionsEditable} in place of the
     * default {@link Editable}.
     */
    public static class MentionsEditableFactory extends Editable.Factory {

        private static MentionsEditableFactory sInstance = new MentionsEditableFactory();

        @NonNull
        public static MentionsEditableFactory getInstance() {
            return sInstance;
        }

        @Override
        @NonNull
        public Editable newEditable(@NonNull CharSequence source) {
            MentionsEditable text = new MentionsEditable(source);
            Selection.setSelection(text, 0);
            return text;
        }
    }

    // --------------------------------------------------
    // MentionSpan Factory
    // --------------------------------------------------

    /**
     * Custom factory used when creating a {@link MentionSpan}.
     */
    public static class MentionSpanFactory {

        @NonNull
        public MentionSpan createMentionSpan(@NonNull Mentionable mention,
                                             @Nullable MentionSpanConfig config) {
            return (config != null) ? new MentionSpan(mention, config) : new MentionSpan(mention);
        }
    }

    // --------------------------------------------------
    // MentionsMovementMethod Class
    // --------------------------------------------------

    /**
     * Custom {@link MovementMethod} for this class used to override specific behavior in {@link ArrowKeyMovementMethod}.
     */
    public static class MentionsMovementMethod extends ArrowKeyMovementMethod {

        private static MentionsMovementMethod sInstance;

        @NonNull
        public static MovementMethod getInstance() {
            if (sInstance == null)
                sInstance = new MentionsMovementMethod();

            return sInstance;
        }

        @Override
        public void initialize(TextView widget, Spannable text) {
            // Do nothing. Note that ArrowKeyMovementMethod calls setSelection(0) here, but we would
            // like to override that behavior (otherwise, cursor is set to beginning of EditText when
            // this method is called).
        }

    }

    // --------------------------------------------------
    // Getters & Setters
    // --------------------------------------------------

    /**
     * Convenience method for getting the {@link MentionsEditable} associated with this class.
     *
     * @return the current text as a {@link MentionsEditable} specifically
     */
    @NonNull
    public MentionsEditable getMentionsText() {
        CharSequence text = super.getText();
        return text instanceof MentionsEditable ? (MentionsEditable) text : new MentionsEditable(text);
    }

    /**
     * @return the {@link Tokenizer} in use
     */
    @Nullable
    public Tokenizer getTokenizer() {
        return mTokenizer;
    }

    /**
     * Sets the tokenizer used by this class. The tokenizer determines how {@link QueryToken} objects
     * are generated.
     *
     * @param tokenizer the {@link Tokenizer} to use
     */
    public void setTokenizer(@Nullable final Tokenizer tokenizer) {
        mTokenizer = tokenizer;
    }

    /**
     * Sets the receiver of query tokens used by this class. The query token receiver will use the
     * tokens to generate suggestions, which can then be inserted back into this edit text.
     *
     * @param queryTokenReceiver the {@link QueryTokenReceiver} to use
     */
    public void setQueryTokenReceiver(@Nullable final QueryTokenReceiver queryTokenReceiver) {
        mQueryTokenReceiver = queryTokenReceiver;
    }

    /**
     * Sets the suggestions manager used by this class.
     *
     * @param suggestionsVisibilityManager the {@link SuggestionsVisibilityManager} to use
     */
    public void setSuggestionsVisibilityManager(@Nullable final SuggestionsVisibilityManager suggestionsVisibilityManager) {
        mSuggestionsVisibilityManager = suggestionsVisibilityManager;
    }

    /**
     * Sets the factory used to create MentionSpans within this class.
     *
     * @param factory the {@link MentionSpanFactory} to use
     */
    public void setMentionSpanFactory(@NonNull final MentionSpanFactory factory) {
        mentionSpanFactory = factory;
    }

    /**
     * Sets the configuration options used when creating MentionSpans.
     *
     * @param config the {@link MentionSpanConfig} to use
     */
    public void setMentionSpanConfig(@NonNull final MentionSpanConfig config) {
        mentionSpanConfig = config;
    }

    /**
     * Sets the string prefix to avoid creating and displaying suggestions.
     *
     * @param prefix prefix to avoid suggestions
     */
    public void setAvoidedPrefix(@Nullable String prefix) {
        mAvoidedPrefix = prefix;
    }

    /**
     * Determines whether the edit text should avoid the current prefix if the user taps on it while
     * it is displaying suggestions (defaults to false).
     *
     * @param avoidPrefixOnTap true if the prefix should be avoided after a tap
     */
    public void setAvoidPrefixOnTap(boolean avoidPrefixOnTap) {
        mAvoidPrefixOnTap = avoidPrefixOnTap;
    }

    // --------------------------------------------------
    // Save & Restore State
    // --------------------------------------------------

    @NonNull
    @Override
    public Parcelable onSaveInstanceState() {
        Parcelable parcelable = super.onSaveInstanceState();
        // Save a copy of MentionsEditable to avoid leaking memory from persisted watchers,
        // as is done in TextView.onSaveInstanceState().
        return new SavedState(parcelable, new MentionsEditable(getMentionsText()));
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }

        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());
        setText(savedState.mentionsEditable);
    }

    /**
     * Convenience class to save/restore the MentionsEditable state.
     */
    protected static class SavedState extends BaseSavedState {
        public MentionsEditable mentionsEditable;

        private SavedState(Parcelable superState, MentionsEditable mentionsEditable) {
            super(superState);
            this.mentionsEditable = mentionsEditable;
        }

        private SavedState(Parcel in) {
            super(in);
            mentionsEditable = in.readParcelable(MentionsEditable.class.getClassLoader());
        }

        @Override
        public void writeToParcel(@NonNull Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeParcelable(mentionsEditable, flags);
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {

            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    // --------------------------------------------------
    // MentionWatcher Interface & Simple Implementation
    // --------------------------------------------------

    /**
     * Interface to receive a callback for mention events.
     */
    public interface MentionWatcher {
        /**
         * Callback for when a mention is added.
         *
         * @param mention   the {@link Mentionable} that was added
         * @param text      the text after the mention was added
         * @param start     the starting index of where the mention was added
         * @param end       the ending index of where the mention was added
         */
        void onMentionAdded(@NonNull Mentionable mention, @NonNull String text, int start, int end);

        /**
         * Callback for when a mention is deleted.
         *
         * @param mention   the {@link Mentionable} that was deleted
         * @param text      the text before the mention was deleted
         * @param start     the starting index of where the mention was deleted
         * @param end       the ending index of where the mention was deleted
         */
        void onMentionDeleted(@NonNull Mentionable mention, @NonNull String text, int start, int end);

        /**
         * Callback for when a mention is partially deleted.
         *
         * @param mention   the {@link Mentionable} that was deleted
         * @param text      the text after the mention was partially deleted
         * @param start     the starting index of where the partial mention starts
         * @param end       the ending index of where the partial mention ends
         */
        void onMentionPartiallyDeleted(@NonNull Mentionable mention, @NonNull String text, int start, int end);
    }

    /**
     * Simple implementation of the {@link com.linkedin.android.spyglass.ui.MentionsEditText.MentionWatcher} interface
     * if you do not want to implement all methods.
     */
    @SuppressWarnings("unused")
    public class SimpleMentionWatcher implements MentionWatcher {
        @Override
        public void onMentionAdded(@NonNull Mentionable mention, @NonNull String text, int start, int end) {}

        @Override
        public void onMentionDeleted(@NonNull Mentionable mention, @NonNull String text, int start, int end) {}

        @Override
        public void onMentionPartiallyDeleted(@NonNull Mentionable mention, @NonNull String text, int start, int end) {}
    }
}
