/*
 * Copyright (C) 2012 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.keyguard;

import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.view.WindowInsets.Type.ime;

import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_ADAPTIVE_AUTH_REQUEST;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_DEVICE_ADMIN;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NONE;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED;
import static com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED;
import static com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Trace;
import android.util.AttributeSet;
import android.view.WindowInsets;
import android.view.WindowInsetsAnimationControlListener;
import android.view.WindowInsetsAnimationController;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.motion.widget.MotionLayout;

import com.android.app.animation.Interpolators;
import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.TextViewInputDisabler;
import com.android.systemui.DejankUtils;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.policy.DevicePostureController;

/**
 * Displays an alphanumeric (latin-1) key entry for the user to enter
 * an unlock password
 */
public class KeyguardPasswordView extends KeyguardAbsKeyInputView {

    private TextView mPasswordEntry;
    private TextViewInputDisabler mPasswordEntryDisabler;
    private DisappearAnimationListener mDisappearAnimationListener;
    @Nullable private MotionLayout mContainerMotionLayout;
    private boolean mAlreadyUsingSplitBouncer = false;
    private boolean mIsLockScreenLandscapeEnabled = false;
    @DevicePostureController.DevicePostureInt
    private int mLastDevicePosture = DEVICE_POSTURE_UNKNOWN;
    private static final int[] DISABLE_STATE_SET = {-android.R.attr.state_enabled};
    private static final int[] ENABLE_STATE_SET = {android.R.attr.state_enabled};

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

    public KeyguardPasswordView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * Use motion layout (new bouncer implementation) if LOCKSCREEN_ENABLE_LANDSCAPE flag is
     * enabled
     */
    public void setIsLockScreenLandscapeEnabled() {
        mIsLockScreenLandscapeEnabled = true;
        findContainerLayout();
    }

    private void findContainerLayout() {
        if (mIsLockScreenLandscapeEnabled) {
            mContainerMotionLayout = findViewById(R.id.password_container);
        }
    }

    @Override
    protected void resetState() {
    }

    @Override
    protected int getPasswordTextViewId() {
        return R.id.passwordEntry;
    }

    @Override
    protected int getPromptReasonStringRes(int reason) {
        switch (reason) {
            case PROMPT_REASON_RESTART:
                return R.string.kg_prompt_reason_restart_password;
            case PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE:
                return R.string.kg_prompt_after_update_password;
            case PROMPT_REASON_TIMEOUT:
                return R.string.kg_prompt_reason_timeout_password;
            case PROMPT_REASON_DEVICE_ADMIN:
                return R.string.kg_prompt_reason_device_admin;
            case PROMPT_REASON_USER_REQUEST:
                return R.string.kg_prompt_after_user_lockdown_password;
            case PROMPT_REASON_PREPARE_FOR_UPDATE:
                return R.string.kg_prompt_added_security_password;
            case PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT:
                return R.string.kg_prompt_reason_timeout_password;
            case PROMPT_REASON_TRUSTAGENT_EXPIRED:
                return R.string.kg_prompt_reason_timeout_password;
            case PROMPT_REASON_ADAPTIVE_AUTH_REQUEST:
                return R.string.kg_prompt_after_adaptive_auth_lock;
            case PROMPT_REASON_NONE:
                return 0;
            default:
                return R.string.kg_prompt_reason_timeout_password;
        }
    }

    void onDevicePostureChanged(@DevicePostureController.DevicePostureInt int posture) {
        if (mLastDevicePosture == posture) return;
        mLastDevicePosture = posture;

        if (mIsLockScreenLandscapeEnabled) {
            boolean useSplitBouncerAfterFold =
                    mLastDevicePosture == DEVICE_POSTURE_CLOSED
                    && getResources().getConfiguration().orientation == ORIENTATION_LANDSCAPE
                    && getResources().getBoolean(R.bool.update_bouncer_constraints);

            if (mAlreadyUsingSplitBouncer != useSplitBouncerAfterFold) {
                updateConstraints(useSplitBouncerAfterFold);
            }
        }

    }

    @Override
    protected void updateConstraints(boolean useSplitBouncer) {
        mAlreadyUsingSplitBouncer = useSplitBouncer;
        if (useSplitBouncer) {
            mContainerMotionLayout.jumpToState(R.id.split_constraints);
            mContainerMotionLayout.setMaxWidth(Integer.MAX_VALUE);
        } else {
            mContainerMotionLayout.jumpToState(R.id.single_constraints);
            mContainerMotionLayout.setMaxWidth(getResources()
                    .getDimensionPixelSize(R.dimen.keyguard_security_width));
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mPasswordEntry = findViewById(getPasswordTextViewId());
        mPasswordEntryDisabler = new TextViewInputDisabler(mPasswordEntry);

        // EditText cursor can fail screenshot tests, so disable it when testing
        if (ActivityManager.isRunningInTestHarness()) {
            mPasswordEntry.setCursorVisible(false);
        }
    }

    @Override
    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
        // send focus to the password field
        return mPasswordEntry.requestFocus(direction, previouslyFocusedRect);
    }

    @Override
    protected void resetPasswordText(boolean animate, boolean announce) {
        mPasswordEntry.setText("");
    }

    @Override
    protected LockscreenCredential getEnteredCredential() {
        return LockscreenCredential.createPasswordOrNone(mPasswordEntry.getText());
    }

    @Override
    protected void setPasswordEntryEnabled(boolean enabled) {
        int color = mPasswordEntry.getTextColors().getColorForState(
                enabled ? ENABLE_STATE_SET : DISABLE_STATE_SET, 0);
        mPasswordEntry.setBackgroundTintList(ColorStateList.valueOf(color));
        mPasswordEntry.setCursorVisible(enabled);
    }

    @Override
    protected void setPasswordEntryInputEnabled(boolean enabled) {
        mPasswordEntryDisabler.setInputEnabled(enabled);
    }

    @Override
    public int getWrongPasswordStringId() {
        return R.string.kg_wrong_password;
    }

    @Override
    public void startAppearAnimation() {
        // Reset state, and let IME animation reveal the view as it slides in, if one exists.
        // It is possible for an IME to have no view, so provide a default animation since no
        // calls to animateForIme would occur
        setAlpha(0f);
        animate()
            .alpha(1f)
            .setDuration(300)
            .start();

        setTranslationY(0f);
    }

    @Override
    public boolean startDisappearAnimation(Runnable finishRunnable) {
        getWindowInsetsController().controlWindowInsetsAnimation(ime(),
                100,
                Interpolators.LINEAR, null, new WindowInsetsAnimationControlListener() {

                    @Override
                    public void onReady(@NonNull WindowInsetsAnimationController controller,
                            int types) {
                        ValueAnimator anim = ValueAnimator.ofFloat(1f, 0f);
                        anim.addUpdateListener(animation -> {
                            if (controller.isCancelled()) {
                                return;
                            }
                            float value = (float) animation.getAnimatedValue();
                            float fraction = anim.getAnimatedFraction();
                            Insets shownInsets = controller.getShownStateInsets();
                            int dist = (int) (-shownInsets.bottom / 4
                                    * fraction);
                            Insets insets = Insets.add(shownInsets, Insets.of(0, 0, 0, dist));
                            if (mDisappearAnimationListener != null) {
                                mDisappearAnimationListener.setTranslationY(-dist);
                            }

                            controller.setInsetsAndAlpha(insets, value, fraction);
                            setAlpha(value);
                        });
                        anim.addListener(new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationStart(Animator animation) {
                            }

                            @Override
                            public void onAnimationEnd(Animator animation) {
                                // Run this in the next frame since it results in a slow binder call
                                // to InputMethodManager#hideSoftInput()
                                DejankUtils.postAfterTraversal(() -> {
                                    Trace.beginSection("KeyguardPasswordView#onAnimationEnd");
                                    // // TODO(b/230620476): Make hideSoftInput oneway
                                    // controller.finish() eventually calls hideSoftInput
                                    controller.finish(false);
                                    runOnFinishImeAnimationRunnable();
                                    finishRunnable.run();
                                    mDisappearAnimationListener = null;
                                    Trace.endSection();
                                });
                            }
                        });
                        anim.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN);
                        anim.start();
                    }

                    @Override
                    public void onFinished(
                            @NonNull WindowInsetsAnimationController controller) {
                    }

                    @Override
                    public void onCancelled(
                            @Nullable WindowInsetsAnimationController controller) {
                        // It is possible to be denied control of ime insets, which means onReady
                        // is never called. We still need to notify the runnables in order to
                        // complete the bouncer disappearing
                        runOnFinishImeAnimationRunnable();
                        finishRunnable.run();
                    }
                });
        return true;
    }

    @Override
    public CharSequence getTitle() {
        return getResources().getString(
                com.android.internal.R.string.keyguard_accessibility_password_unlock);
    }

    @Override
    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        if (!mPasswordEntry.isFocused() && isVisibleToUser()) {
            mPasswordEntry.requestFocus();
        }
        return super.onApplyWindowInsets(insets);
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        super.onWindowFocusChanged(hasWindowFocus);
        if (hasWindowFocus) {
            if (isVisibleToUser()) {
                showKeyboard();
            } else {
                hideKeyboard();
            }
        }
    }

    /**
     * Sends signal to the focused window to show the keyboard.
     */
    public void showKeyboard() {
        post(() -> {
            if (mPasswordEntry.isAttachedToWindow()
                    && !mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
                mPasswordEntry.requestFocus();
                mPasswordEntry.getWindowInsetsController().show(WindowInsets.Type.ime());
            }
        });
    }

    /**
     * Sends signal to the focused window to hide the keyboard.
     */
    public void hideKeyboard() {
        post(() -> {
            if (mPasswordEntry.isAttachedToWindow()
                    && mPasswordEntry.getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
                mPasswordEntry.clearFocus();
                mPasswordEntry.getWindowInsetsController().hide(WindowInsets.Type.ime());
            }
        });
    }

    /**
     * Listens to the progress of the disappear animation and handles it.
     */
    interface DisappearAnimationListener {
        void setTranslationY(int transY);
    }

    /**
     * Set an instance of the disappear animation listener to this class. This will be
     * removed when the animation completes.
     */
    public void setDisappearAnimationListener(DisappearAnimationListener listener) {
        mDisappearAnimationListener = listener;
    }
}
