/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.car.settings.security;

import android.app.Activity;
import android.os.Bundle;
import android.os.UserHandle;
import android.view.View;
import android.widget.TextView;

import androidx.annotation.LayoutRes;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;

import com.android.car.settings.R;
import com.android.car.settings.common.BaseFragment;
import com.android.car.settings.common.Logger;
import com.android.car.ui.toolbar.MenuItem;
import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.LockPatternView;
import com.android.internal.widget.LockPatternView.Cell;
import com.android.internal.widget.LockPatternView.DisplayMode;
import com.android.internal.widget.LockscreenCredential;

import com.google.android.collect.Lists;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Fragment for choosing security lock pattern.
 */
public class ChooseLockPatternFragment extends BaseFragment {

    private static final Logger LOG = new Logger(ChooseLockPatternFragment.class);
    private static final String FRAGMENT_TAG_SAVE_PATTERN_WORKER = "save_pattern_worker";
    private static final String STATE_UI_STAGE = "state_ui_stage";
    private static final String STATE_CHOSEN_PATTERN = "state_chosen_pattern";
    private static final int ID_EMPTY_MESSAGE = -1;
    /**
     * The patten used during the help screen to show how to draw a pattern.
     */
    private final List<LockPatternView.Cell> mAnimatePattern =
            Collections.unmodifiableList(Lists.newArrayList(
                    LockPatternView.Cell.of(0, 0),
                    LockPatternView.Cell.of(0, 1),
                    LockPatternView.Cell.of(1, 1),
                    LockPatternView.Cell.of(2, 1)
            ));
    // How long we wait to clear a wrong pattern
    private int mWrongPatternClearTimeOut;
    private int mUserId;
    private Stage mUiStage = Stage.Introduction;
    private LockPatternView mLockPatternView;
    private TextView mMessageText;
    private LockscreenCredential mChosenPattern;
    private MenuItem mSecondaryButton;
    private MenuItem mPrimaryButton;
    // Existing pattern that user previously set
    private LockscreenCredential mCurrentCredential;
    private SaveLockWorker mSaveLockWorker;
    private Runnable mClearPatternRunnable = () -> mLockPatternView.clearPattern();
    // The pattern listener that responds according to a user choosing a new
    // lock pattern.
    private final LockPatternView.OnPatternListener mChooseNewLockPatternListener =
            new LockPatternView.OnPatternListener() {
                @Override
                public void onPatternStart() {
                    mLockPatternView.removeCallbacks(mClearPatternRunnable);
                    updateUIWhenPatternInProgress();
                }

                @Override
                public void onPatternCleared() {
                    mLockPatternView.removeCallbacks(mClearPatternRunnable);
                }

                @Override
                public void onPatternDetected(List<LockPatternView.Cell> pattern) {
                    switch (mUiStage) {
                        case Introduction:
                        case ChoiceTooShort:
                            handlePatternEntered(pattern);
                            break;
                        case ConfirmWrong:
                        case NeedToConfirm:
                            handleConfirmPattern(pattern);
                            break;
                        default:
                            throw new IllegalStateException("Unexpected stage " + mUiStage
                                    + " when entering the pattern.");
                    }
                }

                @Override
                public void onPatternCellAdded(List<Cell> pattern) {
                }

                private void handleConfirmPattern(List<LockPatternView.Cell> pattern) {
                    if (mChosenPattern == null) {
                        throw new IllegalStateException(
                                "null chosen pattern in stage 'need to confirm");
                    }
                    try (LockscreenCredential credential =
                            LockscreenCredential.createPattern(pattern)) {
                        if (mChosenPattern.equals(credential)) {
                            updateStage(Stage.ChoiceConfirmed);
                        } else {
                            updateStage(Stage.ConfirmWrong);
                        }
                    }
                }

                private void handlePatternEntered(List<LockPatternView.Cell> pattern) {
                    if (pattern.size() < LockPatternUtils.MIN_LOCK_PATTERN_SIZE) {
                        updateStage(Stage.ChoiceTooShort);
                    } else {
                        mChosenPattern = LockscreenCredential.createPattern(pattern);
                        updateStage(Stage.FirstChoiceValid);
                    }
                }
            };

    /**
     * Factory method for creating ChooseLockPatternFragment
     */
    public static ChooseLockPatternFragment newInstance() {
        ChooseLockPatternFragment patternFragment = new ChooseLockPatternFragment();
        return patternFragment;
    }

    @Override
    public List<MenuItem> getToolbarMenuItems() {
        return Arrays.asList(mSecondaryButton, mPrimaryButton);
    }

    @Override
    @LayoutRes
    protected int getLayoutId() {
        return R.layout.choose_lock_pattern;
    }

    @Override
    @StringRes
    protected int getTitleId() {
        return R.string.security_lock_pattern;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mWrongPatternClearTimeOut = getResources().getInteger(R.integer.clear_content_timeout_ms);
        mUserId = UserHandle.myUserId();

        Bundle args = getArguments();
        if (args != null) {
            mCurrentCredential = args.getParcelable(PasswordHelper.EXTRA_CURRENT_SCREEN_LOCK);
            if (mCurrentCredential != null) {
                mCurrentCredential = mCurrentCredential.duplicate();
            }
        }

        if (savedInstanceState != null) {
            mUiStage = Stage.values()[savedInstanceState.getInt(STATE_UI_STAGE)];
            mChosenPattern = savedInstanceState.getParcelable(STATE_CHOSEN_PATTERN);
        }

        mPrimaryButton = new MenuItem.Builder(getContext())
                .setOnClickListener(i -> handlePrimaryButtonClick())
                .build();
        mSecondaryButton = new MenuItem.Builder(getContext())
                .setOnClickListener(i -> handleSecondaryButtonClick())
                .build();
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mMessageText = view.findViewById(R.id.title_text);
        mMessageText.setText(getString(R.string.choose_lock_pattern_message));

        mLockPatternView = view.findViewById(R.id.lockPattern);
        mLockPatternView.setVisibility(View.VISIBLE);
        mLockPatternView.setEnabled(true);
        mLockPatternView.setFadePattern(false);
        mLockPatternView.clearPattern();
        mLockPatternView.setOnPatternListener(mChooseNewLockPatternListener);

        // Re-attach to the exiting worker if there is one.
        if (savedInstanceState != null) {
            mSaveLockWorker = (SaveLockWorker) getFragmentManager().findFragmentByTag(
                    FRAGMENT_TAG_SAVE_PATTERN_WORKER);
        }
    }

    @Override
    public void onStart() {
        super.onStart();
        updateStage(mUiStage);

        if (mSaveLockWorker != null) {
            setPrimaryButtonEnabled(true);
            mSaveLockWorker.setListener(this::onChosenLockSaveFinished);
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt(STATE_UI_STAGE, mUiStage.ordinal());
        outState.putParcelable(STATE_CHOSEN_PATTERN, mChosenPattern);
    }

    @Override
    public void onStop() {
        super.onStop();
        if (mSaveLockWorker != null) {
            mSaveLockWorker.setListener(null);
        }
        getToolbar().getProgressBar().setVisible(false);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        mLockPatternView.clearPattern();

        PasswordHelper.zeroizeCredentials(mChosenPattern, mCurrentCredential);
    }

    /**
     * Updates the messages and buttons appropriate to what stage the user
     * is at in choosing a pattern. This doesn't handle clearing out the pattern;
     * the pattern is expected to be in the right state.
     *
     * @param stage The stage UI should be updated to match with.
     */
    protected void updateStage(Stage stage) {
        mUiStage = stage;

        // Message mText, visibility and
        // mEnabled state all known from the stage
        mMessageText.setText(stage.mMessageId);

        if (stage.mSecondaryButtonState == SecondaryButtonState.Gone) {
            setSecondaryButtonVisible(false);
        } else {
            setSecondaryButtonVisible(true);
            setSecondaryButtonText(stage.mSecondaryButtonState.mTextResId);
            setSecondaryButtonEnabled(stage.mSecondaryButtonState.mEnabled);
        }

        setPrimaryButtonText(stage.mPrimaryButtonState.mText);
        setPrimaryButtonEnabled(stage.mPrimaryButtonState.mEnabled);

        // same for whether the pattern is mEnabled
        if (stage.mPatternEnabled) {
            mLockPatternView.enableInput();
        } else {
            mLockPatternView.disableInput();
        }

        // the rest of the stuff varies enough that it is easier just to handle
        // on a case by case basis.
        mLockPatternView.setDisplayMode(DisplayMode.Correct);

        switch (mUiStage) {
            case Introduction:
                mLockPatternView.clearPattern();
                break;
            case HelpScreen:
                mLockPatternView.setPattern(DisplayMode.Animate, mAnimatePattern);
                break;
            case ChoiceTooShort:
                mLockPatternView.setDisplayMode(DisplayMode.Wrong);
                postClearPatternRunnable();
                break;
            case FirstChoiceValid:
                break;
            case NeedToConfirm:
                mLockPatternView.clearPattern();
                break;
            case ConfirmWrong:
                mLockPatternView.setDisplayMode(DisplayMode.Wrong);
                postClearPatternRunnable();
                break;
            case ChoiceConfirmed:
                break;
            default:
                // Do nothing.
        }
    }

    private void updateUIWhenPatternInProgress() {
        mMessageText.setText(R.string.lockpattern_recording_inprogress);
        setPrimaryButtonEnabled(false);
        setSecondaryButtonEnabled(false);
    }

    // clear the wrong pattern unless they have started a new one
    // already
    private void postClearPatternRunnable() {
        mLockPatternView.removeCallbacks(mClearPatternRunnable);
        mLockPatternView.postDelayed(mClearPatternRunnable, mWrongPatternClearTimeOut);
    }

    private void setPrimaryButtonEnabled(boolean enabled) {
        mPrimaryButton.setEnabled(enabled);
    }

    private void setPrimaryButtonText(@StringRes int textId) {
        mPrimaryButton.setTitle(textId);
    }

    private void setSecondaryButtonVisible(boolean visible) {
        mSecondaryButton.setVisible(visible);
    }

    private void setSecondaryButtonEnabled(boolean enabled) {
        mSecondaryButton.setEnabled(enabled);
    }

    private void setSecondaryButtonText(@StringRes int textId) {
        mSecondaryButton.setTitle(textId);
    }

    // Update display message and decide on next step according to the different mText
    // on the primary button
    private void handlePrimaryButtonClick() {
        switch (mUiStage.mPrimaryButtonState) {
            case Continue:
                if (mUiStage != Stage.FirstChoiceValid) {
                    throw new IllegalStateException("expected ui stage "
                            + Stage.FirstChoiceValid + " when button is "
                            + PrimaryButtonState.Continue);
                }
                updateStage(Stage.NeedToConfirm);
                break;
            case Confirm:
                if (mUiStage != Stage.ChoiceConfirmed) {
                    throw new IllegalStateException("expected ui stage " + Stage.ChoiceConfirmed
                            + " when button is " + PrimaryButtonState.Confirm);
                }
                startSaveAndFinish();
                break;
            case Retry:
                if (mUiStage != Stage.SaveFailure) {
                    throw new IllegalStateException("expected ui stage " + Stage.SaveFailure
                            + " when button is " + PrimaryButtonState.Retry);
                }
                startSaveAndFinish();
                break;
            case Ok:
                if (mUiStage != Stage.HelpScreen) {
                    throw new IllegalStateException("Help screen is only mode with ok button, "
                            + "but stage is " + mUiStage);
                }
                mLockPatternView.clearPattern();
                mLockPatternView.setDisplayMode(DisplayMode.Correct);
                updateStage(Stage.Introduction);
                break;
            default:
                // Do nothing.
        }
    }

    // Update display message and proceed to next step according to the different mText on
    // the secondary button.
    private void handleSecondaryButtonClick() {
        if (mUiStage.mSecondaryButtonState == SecondaryButtonState.Retry) {
            mChosenPattern = null;
            mLockPatternView.clearPattern();
            updateStage(Stage.Introduction);
        } else {
            throw new IllegalStateException("secondary button pressed, but stage of "
                    + mUiStage + " doesn't make sense");
        }
    }

    @VisibleForTesting
    void onChosenLockSaveFinished(boolean isSaveSuccessful) {
        getToolbar().getProgressBar().setVisible(false);

        if (isSaveSuccessful) {
            onComplete();
        } else {
            updateStage(Stage.SaveFailure);
        }
    }

    // Save recorded pattern as an async task and proceed to next
    private void startSaveAndFinish() {
        if (mSaveLockWorker != null && !mSaveLockWorker.isFinished()) {
            LOG.v("startSaveAndFinish with a running SavePatternWorker.");
            return;
        }

        setPrimaryButtonEnabled(false);

        if (mSaveLockWorker == null) {
            mSaveLockWorker = new SaveLockWorker();
            mSaveLockWorker.setListener(this::onChosenLockSaveFinished);

            getFragmentManager()
                    .beginTransaction()
                    .add(mSaveLockWorker, FRAGMENT_TAG_SAVE_PATTERN_WORKER)
                    .commitNow();
        }

        mSaveLockWorker.start(mUserId, mChosenPattern, mCurrentCredential);
        getToolbar().getProgressBar().setVisible(true);
    }

    @VisibleForTesting
    void onComplete() {
        if (mCurrentCredential != null) {
            mCurrentCredential.zeroize();
        }

        getActivity().setResult(Activity.RESULT_OK);
        getActivity().finish();
    }

    /**
     * Keep track internally of where the user is in choosing a pattern.
     */
    enum Stage {
        /**
         * Initial stage when first launching choose a lock pattern.
         * Pattern mEnabled, secondary button allow for Cancel, primary button disabled.
         */
        Introduction(
                R.string.lockpattern_recording_intro_header,
                SecondaryButtonState.Gone,
                PrimaryButtonState.ContinueDisabled,
                /* patternEnabled= */ true),
        /**
         * Help screen to show how a valid pattern looks like.
         * Pattern disabled, primary button shows Ok. No secondary button.
         */
        HelpScreen(
                R.string.lockpattern_settings_help_how_to_record,
                SecondaryButtonState.Gone,
                PrimaryButtonState.Ok,
                /* patternEnabled= */ false),
        /**
         * Invalid pattern is entered, hint message show required number of dots.
         * Secondary button allows for Retry, primary button disabled.
         */
        ChoiceTooShort(
                R.string.lockpattern_recording_incorrect_too_short,
                SecondaryButtonState.Retry,
                PrimaryButtonState.ContinueDisabled,
                /* patternEnabled= */ true),
        /**
         * First drawing on the pattern is valid, primary button shows Continue,
         * can proceed to next screen.
         */
        FirstChoiceValid(
                R.string.lockpattern_recording_intro_header,
                SecondaryButtonState.Retry,
                PrimaryButtonState.Continue,
                /* patternEnabled= */ false),
        /**
         * Need to draw pattern again to confirm.
         * Secondary button allows for Cancel, primary button disabled.
         */
        NeedToConfirm(
                R.string.lockpattern_need_to_confirm,
                SecondaryButtonState.Gone,
                PrimaryButtonState.ConfirmDisabled,
                /* patternEnabled= */ true),
        /**
         * Confirmation of previous drawn pattern failed, didn't enter the same pattern.
         * Need to re-draw the pattern to match the fist pattern.
         */
        ConfirmWrong(
                R.string.lockpattern_pattern_wrong,
                SecondaryButtonState.Gone,
                PrimaryButtonState.ConfirmDisabled,
                /* patternEnabled= */ true),
        /**
         * Pattern is confirmed after drawing the same pattern twice.
         * Pattern disabled.
         */
        ChoiceConfirmed(
                R.string.lockpattern_pattern_confirmed,
                SecondaryButtonState.Gone,
                PrimaryButtonState.Confirm,
                /* patternEnabled= */ false),

        /**
         * Error saving pattern.
         * Pattern disabled, primary button shows Retry, secondary button allows for cancel
         */
        SaveFailure(
                R.string.error_saving_lockpattern,
                SecondaryButtonState.Gone,
                PrimaryButtonState.Retry,
                /* patternEnabled= */ false);

        final int mMessageId;
        final SecondaryButtonState mSecondaryButtonState;
        final PrimaryButtonState mPrimaryButtonState;
        final boolean mPatternEnabled;

        /**
         * @param messageId            The message displayed as instruction.
         * @param secondaryButtonState The state of the secondary button.
         * @param primaryButtonState   The state of the primary button.
         * @param patternEnabled       Whether the pattern widget is mEnabled.
         */
        Stage(@StringRes int messageId,
                SecondaryButtonState secondaryButtonState,
                PrimaryButtonState primaryButtonState,
                boolean patternEnabled) {
            this.mMessageId = messageId;
            this.mSecondaryButtonState = secondaryButtonState;
            this.mPrimaryButtonState = primaryButtonState;
            this.mPatternEnabled = patternEnabled;
        }
    }

    /**
     * The states of the primary footer button.
     */
    enum PrimaryButtonState {
        Continue(R.string.continue_button_text, true),
        ContinueDisabled(R.string.continue_button_text, false),
        Confirm(R.string.lockpattern_confirm_button_text, true),
        ConfirmDisabled(R.string.lockpattern_confirm_button_text, false),
        Retry(R.string.lockscreen_retry_button_text, true),
        Ok(R.string.okay, true);

        final int mText;
        final boolean mEnabled;

        /**
         * @param text    The displayed mText for this mode.
         * @param enabled Whether the button should be mEnabled.
         */
        PrimaryButtonState(@StringRes int text, boolean enabled) {
            this.mText = text;
            this.mEnabled = enabled;
        }
    }

    /**
     * The states of the secondary footer button.
     */
    enum SecondaryButtonState {
        Retry(R.string.lockpattern_retry_button_text, true),
        Gone(ID_EMPTY_MESSAGE, false);

        final int mTextResId;
        final boolean mEnabled;

        /**
         * @param textId  The displayed mText for this mode.
         * @param enabled Whether the button should be mEnabled.
         */
        SecondaryButtonState(@StringRes int textId, boolean enabled) {
            this.mTextResId = textId;
            this.mEnabled = enabled;
        }
    }
}
