/*
 * Copyright (C) 2014 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 com.android.tv.settings.dialog;

import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.OverScroller;
import android.widget.TextView;
import android.widget.Toast;

import androidx.annotation.IntDef;
import androidx.fragment.app.Fragment;

import com.android.tv.settings.R;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.function.Consumer;

public abstract class PinDialogFragment extends SafeDismissDialogFragment
        implements DialogInterface.OnDismissListener {
    private static final String TAG = PinDialogFragment.class.getSimpleName();
    private static final boolean DEBUG = false;

    protected static final String ARG_TYPE = "type";

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({PIN_DIALOG_TYPE_UNLOCK_CHANNEL,
            PIN_DIALOG_TYPE_UNLOCK_PROGRAM,
            PIN_DIALOG_TYPE_ENTER_PIN,
            PIN_DIALOG_TYPE_NEW_PIN,
            PIN_DIALOG_TYPE_OLD_PIN,
            PIN_DIALOG_TYPE_DELETE_PIN})
    public @interface PinDialogType {}
    /**
     * PIN code dialog for unlock channel
     */
    public static final int PIN_DIALOG_TYPE_UNLOCK_CHANNEL = 0;

    /**
     * PIN code dialog for unlock content.
     * Only difference between {@code PIN_DIALOG_TYPE_UNLOCK_CHANNEL} is it's title.
     */
    public static final int PIN_DIALOG_TYPE_UNLOCK_PROGRAM = 1;

    /**
     * PIN code dialog for change parental control settings
     */
    public static final int PIN_DIALOG_TYPE_ENTER_PIN = 2;

    /**
     * PIN code dialog for set new PIN
     */
    public static final int PIN_DIALOG_TYPE_NEW_PIN = 3;

    // PIN code dialog for checking old PIN. This is intenal only.
    private static final int PIN_DIALOG_TYPE_OLD_PIN = 4;

    /**
     * PIN code dialog for deleting the PIN
     */
    public static final int PIN_DIALOG_TYPE_DELETE_PIN = 5;

    private static final int PIN_DIALOG_RESULT_SUCCESS = 0;
    private static final int PIN_DIALOG_RESULT_FAIL = 1;

    private static final int MAX_WRONG_PIN_COUNT = 5;
    private static final int WRONG_PIN_REFRESH_DELAY = 1000;
    private static final int DISABLE_PIN_DURATION_MILLIS = 60 * 1000; // 1 minute

    public interface ResultListener {
        void pinFragmentDone(int requestCode, boolean success);
    }

    public static final String DIALOG_TAG = PinDialogFragment.class.getName();

    private static final int NUMBER_PICKERS_RES_ID[] = {
            R.id.first, R.id.second, R.id.third, R.id.fourth };

    private int mType;
    private int mRetCode;

    private TextView mWrongPinView;
    private View mEnterPinView;
    private TextView mTitleView;
    private PinNumberPicker[] mPickers;
    private EditText mAccessiblePin;
    private String mOriginalPin;
    private String mPrevPin;
    private int mWrongPinCount;
    private long mDisablePinUntil;
    private boolean mIsPinSet;
    private boolean mIsDispatched;

    private final Handler mHandler = new Handler();

    /**
     * Get the bad PIN retry time
     * @return Retry time
     */
    public abstract long getPinDisabledUntil();

    /**
     * Set the bad PIN retry time
     * @param retryDisableTimeout Retry time
     */
    public abstract void setPinDisabledUntil(long retryDisableTimeout);

    /**
     * Set PIN password for the profile
     * @param pin New PIN password
     * @param consumer Will be called with the success result from setting the pin
     */
    public abstract void setPin(String pin, String originalPin, Consumer<Boolean> consumer);

    /**
     * Delete PIN password for the profile
     * @param oldPin Old PIN password (required)
     * @param consumer Will be called with the success result from deleting the pin
     */
    public abstract void deletePin(String oldPin, Consumer<Boolean> consumer);

    /**
     * Validate PIN password for the profile
     * @param pin Password to check
     * @param consumer Will be called with the result of the check
     */
    public abstract void isPinCorrect(String pin, Consumer<Boolean> consumer);

    /**
     * Check if there is a PIN password set on the profile
     * @param consumer Will be called with the result of the check
     */
    public abstract void isPinSet(Consumer<Boolean> consumer);

    public PinDialogFragment() {
        mRetCode = PIN_DIALOG_RESULT_FAIL;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setStyle(STYLE_NO_TITLE, 0);
        mDisablePinUntil = getPinDisabledUntil();
        final Bundle args = getArguments();
        if (!args.containsKey(ARG_TYPE)) {
            throw new IllegalStateException("Fragment arguments must specify type");
        }
        mType = getArguments().getInt(ARG_TYPE);
    }

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Dialog dlg = super.onCreateDialog(savedInstanceState);
        dlg.getWindow().getAttributes().windowAnimations = R.style.pin_dialog_animation;
        PinNumberPicker.loadResources(dlg.getContext());
        return dlg;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        final View v = inflater.inflate(R.layout.pin_dialog, container, false);

        mWrongPinView = v.findViewById(R.id.wrong_pin);
        mEnterPinView = v.findViewById(R.id.enter_pin);
        if (mEnterPinView == null) {
            throw new IllegalStateException("R.id.enter_pin missing!");
        }
        mTitleView = mEnterPinView.findViewById(R.id.title);
        isPinSet(result -> dispatchOnIsPinSet(result, savedInstanceState, v));

        return v;
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        if (DEBUG) Log.d(TAG, "onDismiss: mRetCode=" + mRetCode);

        boolean result = mRetCode == PIN_DIALOG_RESULT_SUCCESS;
        Fragment f = getTargetFragment();
        if (f instanceof ResultListener) {
            ((ResultListener) f).pinFragmentDone(getTargetRequestCode(), result);
        } else if (getActivity() instanceof ResultListener) {
            final ResultListener listener = (ResultListener) getActivity();
            listener.pinFragmentDone(getTargetRequestCode(), result);
        }
    }

    private void updateWrongPin() {
        if (getActivity() == null) {
            // The activity is already detached. No need to update.
            mHandler.removeCallbacks(null);
            return;
        }

        final long secondsLeft = (mDisablePinUntil - System.currentTimeMillis()) / 1000;
        final boolean enabled = secondsLeft < 1;
        if (enabled) {
            mWrongPinView.setVisibility(View.GONE);
            mEnterPinView.setVisibility(View.VISIBLE);
            mWrongPinCount = 0;
        } else {
            mEnterPinView.setVisibility(View.GONE);
            mWrongPinView.setVisibility(View.VISIBLE);
            mWrongPinView.setText(getResources().getString(R.string.pin_enter_wrong_seconds,
                    secondsLeft));
            mHandler.postDelayed(this::updateWrongPin, WRONG_PIN_REFRESH_DELAY);
        }
    }

    private void exit(int retCode) {
        mRetCode = retCode;
        mIsDispatched = false;
        dismiss();
    }

    private void handleWrongPin() {
        if (++mWrongPinCount >= MAX_WRONG_PIN_COUNT) {
            mDisablePinUntil = System.currentTimeMillis() + DISABLE_PIN_DURATION_MILLIS;
            setPinDisabledUntil(mDisablePinUntil);
            updateWrongPin();
        } else {
            showToast(R.string.pin_toast_wrong);
        }
    }

    private void showToast(int resId) {
        Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show();
    }

    private void done(String pin) {
        if (DEBUG) Log.d(TAG, "done: mType=" + mType + " pin=" + pin);
        if (mIsDispatched) {
            // avoid re-triggering any of the dispatch methods if the user
            // double clicks in the pin dialog
            return;
        }
        switch (mType) {
            case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
            case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
            case PIN_DIALOG_TYPE_ENTER_PIN:
                dispatchOnPinEntered(pin);
                break;
            case PIN_DIALOG_TYPE_DELETE_PIN:
                dispatchOnDeletePin(pin);
                break;
            case PIN_DIALOG_TYPE_NEW_PIN:
                dispatchOnNewPinTyped(pin);
                break;
            case PIN_DIALOG_TYPE_OLD_PIN:
                dispatchOnOldPinTyped(pin);
                break;
        }
    }

    private void dispatchOnPinEntered(String pin) {
        isPinCorrect(pin, pinCorrect -> {
            if (!mIsPinSet || pinCorrect) {
                exit(PIN_DIALOG_RESULT_SUCCESS);
            } else {
                resetPinInput();
                handleWrongPin();
            }
        });
    }

    private void dispatchOnDeletePin(String pin) {
        isPinCorrect(pin, pinIsCorrect -> {
            if (pinIsCorrect) {
                mIsDispatched = true;
                deletePin(pin, success -> {
                    exit(success ? PIN_DIALOG_RESULT_SUCCESS : PIN_DIALOG_RESULT_FAIL);
                });
            } else {
                resetPinInput();
                handleWrongPin();
            }
        });
    }

    private void dispatchOnNewPinTyped(String pin) {
        if (mPrevPin == null) {
            resetPinInput();
            mPrevPin = pin;
            mTitleView.setText(R.string.pin_enter_again);
        } else {
            if (pin.equals(mPrevPin)) {
                mIsDispatched = true;
                setPin(pin, mOriginalPin, success -> {
                    exit(PIN_DIALOG_RESULT_SUCCESS);
                });
            } else {
                resetPinInput();
                mTitleView.setText(R.string.pin_enter_new_pin);
                mPrevPin = null;
                showToast(R.string.pin_toast_not_match);
            }
        }
    }

    private void dispatchOnOldPinTyped(String pin) {
        resetPinInput();
        isPinCorrect(pin, pinIsCorrect -> {
            if (isAdded()) {
                if (pinIsCorrect) {
                    mOriginalPin = pin;
                    mType = PIN_DIALOG_TYPE_NEW_PIN;
                    mTitleView.setText(R.string.pin_enter_new_pin);
                } else {
                    handleWrongPin();
                }
            }
        });
    }

    public int getType() {
        return mType;
    }

    private void dispatchOnIsPinSet(Boolean result, Bundle savedInstanceState, View v) {
        mIsPinSet = result;
        if (!mIsPinSet) {
            // If PIN isn't set, user should set a PIN.
            // Successfully setting a new set is considered as entering correct PIN.
            mType = PIN_DIALOG_TYPE_NEW_PIN;
        }

        mEnterPinView.setVisibility(View.VISIBLE);
        setDialogTitle();
        setUpPinNumberPicker(savedInstanceState, v);
    }

    private void setDialogTitle() {
        switch (mType) {
            case PIN_DIALOG_TYPE_UNLOCK_CHANNEL:
                mTitleView.setText(R.string.pin_enter_unlock_channel);
                break;
            case PIN_DIALOG_TYPE_UNLOCK_PROGRAM:
                mTitleView.setText(R.string.pin_enter_unlock_program);
                break;
            case PIN_DIALOG_TYPE_ENTER_PIN:
            case PIN_DIALOG_TYPE_DELETE_PIN:
                mTitleView.setText(R.string.pin_enter_pin);
                break;
            case PIN_DIALOG_TYPE_NEW_PIN:
                if (!mIsPinSet) {
                    mTitleView.setText(R.string.pin_enter_new_pin);
                } else {
                    mTitleView.setText(R.string.pin_enter_old_pin);
                    mType = PIN_DIALOG_TYPE_OLD_PIN;
                }
        }
    }

    private void setUpPinNumberPicker(Bundle savedInstanceState, View v) {
        if (mType != PIN_DIALOG_TYPE_NEW_PIN) {
            updateWrongPin();
        }

        AccessibilityManager accessibilityManager = getContext().getSystemService(
                AccessibilityManager.class);
        // Use built in EditText in accessibility mode as it can use custom input methods
        // and other needed behaviors.
        boolean isAccessible = accessibilityManager.isEnabled() &&
                accessibilityManager.isTouchExplorationEnabled();

        mAccessiblePin = v.requireViewById(R.id.accessible_pin);
        mAccessiblePin.setVisibility(isAccessible ? View.VISIBLE : View.GONE);
        mAccessiblePin.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                if (s.length() == 4) {
                    done(s.toString());
                }
            }

            @Override
            public void afterTextChanged(Editable s) {
            }
        });

        mPickers = new PinNumberPicker[NUMBER_PICKERS_RES_ID.length];
        for (int i = 0; i < NUMBER_PICKERS_RES_ID.length; i++) {
            mPickers[i] = v.findViewById(NUMBER_PICKERS_RES_ID[i]);
            mPickers[i].setValueRange(0, 9);
            mPickers[i].setPinDialogFragment(this);
            mPickers[i].updateFocus();
            mPickers[i].setVisibility(isAccessible ? View.GONE : View.VISIBLE);
        }
        for (int i = 0; i < NUMBER_PICKERS_RES_ID.length - 1; i++) {
            mPickers[i].setNextNumberPicker(mPickers[i + 1]);
        }

        if (savedInstanceState == null) {
            (isAccessible ? mAccessiblePin : mPickers[0]).requestFocus();
        }
    }

    private String getPinInput() {
        String result = "";
        try {
            for (PinNumberPicker pnp : mPickers) {
                result += pnp.getValue();
            }
        } catch (IllegalStateException e) {
            result = "";
        }
        return result;
    }

    private void resetPinInput() {
        for (PinNumberPicker pnp : mPickers) {
            pnp.setValueRange(0, 9);
        }
        (mAccessiblePin.getVisibility() == View.VISIBLE ? mAccessiblePin : mPickers[0])
                .requestFocus();
        mAccessiblePin.setText("");
    }

    public static final class PinNumberPicker extends FrameLayout {
        private static final int NUMBER_VIEWS_RES_ID[] = {
            R.id.previous2_number,
            R.id.previous_number,
            R.id.current_number,
            R.id.next_number,
            R.id.next2_number };
        private static final int CURRENT_NUMBER_VIEW_INDEX = 2;

        private static Animator sFocusedNumberEnterAnimator;
        private static Animator sFocusedNumberExitAnimator;
        private static Animator sAdjacentNumberEnterAnimator;
        private static Animator sAdjacentNumberExitAnimator;

        private static float sAlphaForFocusedNumber;
        private static float sAlphaForAdjacentNumber;

        private int mMinValue;
        private int mMaxValue;
        private int mCurrentValue;
        private int mNextValue;
        private final int mNumberViewHeight;
        private PinDialogFragment mDialog;
        private PinNumberPicker mNextNumberPicker;
        private boolean mCancelAnimation;

        private final View mNumberViewHolder;
        private final View mBackgroundView;
        private final TextView[] mNumberViews;
        private final OverScroller mScroller;

        public PinNumberPicker(Context context) {
            this(context, null);
        }

        public PinNumberPicker(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }

        public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
            this(context, attrs, defStyleAttr, 0);
        }

        public PinNumberPicker(Context context, AttributeSet attrs, int defStyleAttr,
                int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            View view = inflate(context, R.layout.pin_number_picker, this);
            mNumberViewHolder = view.findViewById(R.id.number_view_holder);
            if (mNumberViewHolder == null) {
                throw new IllegalStateException("R.id.number_view_holder missing!");
            }
            mBackgroundView = view.findViewById(R.id.focused_background);
            mNumberViews = new TextView[NUMBER_VIEWS_RES_ID.length];
            for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
                mNumberViews[i] = view.findViewById(NUMBER_VIEWS_RES_ID[i]);
            }
            Resources resources = context.getResources();
            mNumberViewHeight = resources.getDimensionPixelOffset(
                    R.dimen.pin_number_picker_text_view_height);

            mScroller = new OverScroller(context);

            mNumberViewHolder.setOnFocusChangeListener((v, hasFocus) -> updateFocus());

            mNumberViewHolder.setOnKeyListener((v, keyCode, event) -> {
                if (event.getAction() == KeyEvent.ACTION_DOWN) {
                    switch (keyCode) {
                        case KeyEvent.KEYCODE_DPAD_UP:
                        case KeyEvent.KEYCODE_DPAD_DOWN: {
                            if (!mScroller.isFinished() || mCancelAnimation) {
                                endScrollAnimation();
                            }
                            if (mScroller.isFinished() || mCancelAnimation) {
                                mCancelAnimation = false;
                                if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
                                    mNextValue = adjustValueInValidRange(mCurrentValue + 1);
                                    startScrollAnimation(true);
                                    mScroller.startScroll(0, 0, 0, mNumberViewHeight,
                                            getResources().getInteger(
                                                    R.integer.pin_number_scroll_duration));
                                } else {
                                    mNextValue = adjustValueInValidRange(mCurrentValue - 1);
                                    startScrollAnimation(false);
                                    mScroller.startScroll(0, 0, 0, -mNumberViewHeight,
                                            getResources().getInteger(
                                                    R.integer.pin_number_scroll_duration));
                                }
                                updateText();
                                invalidate();
                            }
                            return true;
                        }
                    }
                } else if (event.getAction() == KeyEvent.ACTION_UP) {
                    switch (keyCode) {
                        case KeyEvent.KEYCODE_DPAD_UP:
                        case KeyEvent.KEYCODE_DPAD_DOWN: {
                            mCancelAnimation = true;
                            return true;
                        }
                    }
                }
                return false;
            });
            mNumberViewHolder.setScrollY(mNumberViewHeight);
        }

        static void loadResources(Context context) {
            if (sFocusedNumberEnterAnimator == null) {
                TypedValue outValue = new TypedValue();
                context.getResources().getValue(
                        R.dimen.pin_alpha_for_focused_number, outValue, true);
                sAlphaForFocusedNumber = outValue.getFloat();
                context.getResources().getValue(
                        R.dimen.pin_alpha_for_adjacent_number, outValue, true);
                sAlphaForAdjacentNumber = outValue.getFloat();

                sFocusedNumberEnterAnimator = AnimatorInflater.loadAnimator(context,
                        R.animator.pin_focused_number_enter);
                sFocusedNumberExitAnimator = AnimatorInflater.loadAnimator(context,
                        R.animator.pin_focused_number_exit);
                sAdjacentNumberEnterAnimator = AnimatorInflater.loadAnimator(context,
                        R.animator.pin_adjacent_number_enter);
                sAdjacentNumberExitAnimator = AnimatorInflater.loadAnimator(context,
                        R.animator.pin_adjacent_number_exit);
            }
        }

        @Override
        public void computeScroll() {
            super.computeScroll();
            if (mScroller.computeScrollOffset()) {
                mNumberViewHolder.setScrollY(mScroller.getCurrY() + mNumberViewHeight);
                updateText();
                invalidate();
            } else if (mCurrentValue != mNextValue) {
                mCurrentValue = mNextValue;
            }
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            if (event.getAction() == KeyEvent.ACTION_UP) {
                int keyCode = event.getKeyCode();
                if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
                    jumpNextValue(keyCode - KeyEvent.KEYCODE_0);
                } else if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER
                        && keyCode != KeyEvent.KEYCODE_ENTER) {
                    return super.dispatchKeyEvent(event);
                }
                if (mNextNumberPicker == null) {
                    String pin = mDialog.getPinInput();
                    if (!TextUtils.isEmpty(pin)) {
                        mDialog.done(pin);
                    }
                } else {
                    mNextNumberPicker.requestFocus();
                }
                return true;
            }
            return super.dispatchKeyEvent(event);
        }

        @Override
        public void setEnabled(boolean enabled) {
            super.setEnabled(enabled);
            mNumberViewHolder.setFocusable(enabled);
            for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
                mNumberViews[i].setEnabled(enabled);
            }
        }

        void startScrollAnimation(boolean scrollUp) {
            if (scrollUp) {
                sAdjacentNumberExitAnimator.setTarget(mNumberViews[1]);
                sFocusedNumberExitAnimator.setTarget(mNumberViews[2]);
                sFocusedNumberEnterAnimator.setTarget(mNumberViews[3]);
                sAdjacentNumberEnterAnimator.setTarget(mNumberViews[4]);
            } else {
                sAdjacentNumberEnterAnimator.setTarget(mNumberViews[0]);
                sFocusedNumberEnterAnimator.setTarget(mNumberViews[1]);
                sFocusedNumberExitAnimator.setTarget(mNumberViews[2]);
                sAdjacentNumberExitAnimator.setTarget(mNumberViews[3]);
            }
            sAdjacentNumberExitAnimator.start();
            sFocusedNumberExitAnimator.start();
            sFocusedNumberEnterAnimator.start();
            sAdjacentNumberEnterAnimator.start();
        }

        void endScrollAnimation() {
            sAdjacentNumberExitAnimator.end();
            sFocusedNumberExitAnimator.end();
            sFocusedNumberEnterAnimator.end();
            sAdjacentNumberEnterAnimator.end();
            mCurrentValue = mNextValue;
            mNumberViews[1].setAlpha(sAlphaForAdjacentNumber);
            mNumberViews[2].setAlpha(sAlphaForFocusedNumber);
            mNumberViews[3].setAlpha(sAlphaForAdjacentNumber);
        }

        void setValueRange(int min, int max) {
            if (min > max) {
                throw new IllegalArgumentException(
                        "The min value should be greater than or equal to the max value");
            }
            mMinValue = min;
            mMaxValue = max;
            mNextValue = mCurrentValue = mMinValue - 1;
            clearText();
            mNumberViews[CURRENT_NUMBER_VIEW_INDEX].setText("—");
        }

        void setPinDialogFragment(PinDialogFragment dlg) {
            mDialog = dlg;
        }

        void setNextNumberPicker(PinNumberPicker picker) {
            mNextNumberPicker = picker;
        }

        int getValue() {
            if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
                throw new IllegalStateException("Value is not set");
            }
            return mCurrentValue;
        }

        void jumpNextValue(int value) {
            if (value < mMinValue || value > mMaxValue) {
                throw new IllegalStateException("Value is not set");
            }
            mNextValue = mCurrentValue = adjustValueInValidRange(value);
            updateText();
        }

        void updateFocus() {
            endScrollAnimation();
            if (mNumberViewHolder.isFocused()) {
                mBackgroundView.setVisibility(View.VISIBLE);
                updateText();
            } else {
                mBackgroundView.setVisibility(View.GONE);
                if (!mScroller.isFinished()) {
                    mCurrentValue = mNextValue;
                    mScroller.abortAnimation();
                }
                clearText();
                mNumberViewHolder.setScrollY(mNumberViewHeight);
            }
        }

        private void clearText() {
            for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
                if (i != CURRENT_NUMBER_VIEW_INDEX) {
                    mNumberViews[i].setText("");
                } else if (mCurrentValue >= mMinValue && mCurrentValue <= mMaxValue) {
                    // Bullet
                    mNumberViews[i].setText("\u2022");
                }
            }
        }

        private void updateText() {
            if (mNumberViewHolder.isFocused()) {
                if (mCurrentValue < mMinValue || mCurrentValue > mMaxValue) {
                    mNextValue = mCurrentValue = mMinValue;
                }
                int value = adjustValueInValidRange(mCurrentValue - CURRENT_NUMBER_VIEW_INDEX);
                for (int i = 0; i < NUMBER_VIEWS_RES_ID.length; ++i) {
                    mNumberViews[i].setText(String.valueOf(adjustValueInValidRange(value)));
                    value = adjustValueInValidRange(value + 1);
                }
            }
        }

        private int adjustValueInValidRange(int value) {
            int interval = mMaxValue - mMinValue + 1;
            if (value < mMinValue - interval || value > mMaxValue + interval) {
                throw new IllegalArgumentException("The value( " + value
                        + ") is too small or too big to adjust");
            }
            return (value < mMinValue) ? value + interval
                    : (value > mMaxValue) ? value - interval : value;
        }
    }
}
