// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.signin;

import android.accounts.Account;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Handler;
import android.util.Log;

import org.chromium.base.CalledByNative;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.chrome.browser.invalidation.InvalidationController;
import org.chromium.chrome.browser.sync.ProfileSyncService;
import org.chromium.chrome.R;
import org.chromium.sync.internal_api.pub.base.ModelType;
import org.chromium.sync.notifier.SyncStatusHelper;
import org.chromium.sync.signin.ChromeSigninController;

import java.util.HashSet;

/**
 * Android wrapper of the SigninManager which provides access from the Java layer.
 * <p/>
 * This class handles common paths during the sign-in and sign-out flows.
 * <p/>
 * Only usable from the UI thread as the native SigninManager requires its access to be in the
 * UI thread.
 * <p/>
 * See chrome/browser/signin/signin_manager_android.h for more details.
 */
public class SigninManager {

    private static final String TAG = "SigninManager";

    private static SigninManager sSigninManager;

    private final Context mContext;
    private final int mNativeSigninManagerAndroid;

    /** Tracks whether the First Run check has been completed.
     *
     * A new sign-in can not be started while this is pending, to prevent the
     * pending check from eventually starting a 2nd sign-in.
     */
    private boolean mFirstRunCheckIsPending = true;
    private ObserverList<SignInAllowedObserver> mSignInAllowedObservers =
            new ObserverList<SignInAllowedObserver>();

    private Activity mSignInActivity;
    private Account mSignInAccount;
    private Observer mSignInObserver;
    private boolean mPassive = false;

    private ProgressDialog mSignOutProgressDialog;
    private Runnable mSignOutCallback;

    private AlertDialog mPolicyConfirmationDialog;

    /**
     * SignInAllowedObservers will be notified once signing-in becomes allowed or disallowed.
     */
    public static interface SignInAllowedObserver {
        /**
         * Invoked once all startup checks are done and signing-in becomes allowed, or disallowed.
         */
        public void onSignInAllowedChanged();
    }

    /**
     * The Observer of startSignIn() will be notified when sign-in completes.
     */
    public static interface Observer {
        /**
         * Invoked after sign-in completed successfully.
         */
        public void onSigninComplete();

        /**
         * Invoked when the sign-in process was cancelled by the user.
         *
         * The user should have the option of going back and starting the process again,
         * if possible.
         */
        public void onSigninCancelled();
    }

    /**
     * A helper method for retrieving the application-wide SigninManager.
     * <p/>
     * Can only be accessed on the main thread.
     *
     * @param context the ApplicationContext is retrieved from the context used as an argument.
     * @return a singleton instance of the SigninManager.
     */
    public static SigninManager get(Context context) {
        ThreadUtils.assertOnUiThread();
        if (sSigninManager == null) {
            sSigninManager = new SigninManager(context);
        }
        return sSigninManager;
    }

    private SigninManager(Context context) {
        ThreadUtils.assertOnUiThread();
        mContext = context.getApplicationContext();
        mNativeSigninManagerAndroid = nativeInit();
    }

    /**
     * Notifies the SigninManager that the First Run check has completed.
     *
     * The user will be allowed to sign-in once this is signaled.
     */
    public void onFirstRunCheckDone() {
        mFirstRunCheckIsPending = false;

        if (isSignInAllowed()) {
            notifySignInAllowedChanged();
        }
    }

    /**
     * Returns true if signin can be started now.
     */
    public boolean isSignInAllowed() {
        return !mFirstRunCheckIsPending &&
                mSignInAccount == null &&
                ChromeSigninController.get(mContext).getSignedInUser() == null;
    }

    public void addSignInAllowedObserver(SignInAllowedObserver observer) {
        mSignInAllowedObservers.addObserver(observer);
    }

    public void removeSignInAllowedObserver(SignInAllowedObserver observer) {
        mSignInAllowedObservers.removeObserver(observer);
    }

    private void notifySignInAllowedChanged() {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                for (SignInAllowedObserver observer : mSignInAllowedObservers) {
                    observer.onSignInAllowedChanged();
                }
            }
        });
    }

    /**
     * Starts the sign-in flow, and executes the callback when ready to proceed.
     * <p/>
     * This method checks with the native side whether the account has management enabled, and may
     * present a dialog to the user to confirm sign-in. The callback is invoked once these processes
     * and the common sign-in initialization complete.
     *
     * @param activity The context to use for the operation.
     * @param account The account to sign in to.
     * @param passive If passive is true then this operation should not interact with the user.
     * @param callback The Observer to notify when the sign-in process is finished.
     */
    public void startSignIn(
            Activity activity, final Account account, boolean passive, final Observer observer) {
        assert mSignInActivity == null;
        assert mSignInAccount == null;
        assert mSignInObserver == null;

        if (mFirstRunCheckIsPending) {
            Log.w(TAG, "Ignoring sign-in request until the First Run check completes.");
            return;
        }

        mSignInActivity = activity;
        mSignInAccount = account;
        mSignInObserver = observer;
        mPassive = passive;

        notifySignInAllowedChanged();

        if (!nativeShouldLoadPolicyForUser(account.name)) {
            // Proceed with the sign-in flow without checking for policy if it can be determined
            // that this account can't have management enabled based on the username.
            doSignIn();
            return;
        }

        Log.d(TAG, "Checking if account has policy management enabled");
        // This will call back to onPolicyCheckedBeforeSignIn.
        nativeCheckPolicyBeforeSignIn(mNativeSigninManagerAndroid, account.name);
    }

    @CalledByNative
    private void onPolicyCheckedBeforeSignIn(String managementDomain) {
        if (managementDomain == null) {
            Log.d(TAG, "Account doesn't have policy");
            doSignIn();
            return;
        }

        if (mSignInActivity.isDestroyed()) {
            // The activity is no longer running, cancel sign in.
            cancelSignIn();
            return;
        }

        if (mPassive) {
            // If this is a passive interaction (e.g. auto signin) then don't show the confirmation
            // dialog.
            nativeFetchPolicyBeforeSignIn(mNativeSigninManagerAndroid);
            return;
        }

        Log.d(TAG, "Account has policy management");
        AlertDialog.Builder builder = new AlertDialog.Builder(mSignInActivity);
        builder.setTitle(R.string.policy_dialog_title);
        builder.setMessage(mContext.getResources().getString(R.string.policy_dialog_message,
                                                             managementDomain));
        builder.setPositiveButton(
                R.string.policy_dialog_proceed,
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int id) {
                        Log.d(TAG, "Accepted policy management, proceeding with sign-in");
                        // This will call back to onPolicyFetchedBeforeSignIn.
                        nativeFetchPolicyBeforeSignIn(mNativeSigninManagerAndroid);
                        mPolicyConfirmationDialog = null;
                    }
                });
        builder.setNegativeButton(
                R.string.policy_dialog_cancel,
                new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int id) {
                        Log.d(TAG, "Cancelled sign-in");
                        cancelSignIn();
                        mPolicyConfirmationDialog = null;
                    }
                });
        builder.setOnDismissListener(
                new DialogInterface.OnDismissListener() {
                    @Override
                    public void onDismiss(DialogInterface dialog) {
                        if (mPolicyConfirmationDialog != null) {
                            Log.d(TAG, "Policy dialog dismissed, cancelling sign-in.");
                            cancelSignIn();
                            mPolicyConfirmationDialog = null;
                        }
                    }
                });
        mPolicyConfirmationDialog = builder.create();
        mPolicyConfirmationDialog.show();
    }

    @CalledByNative
    private void onPolicyFetchedBeforeSignIn() {
        // Policy has been fetched for the user and is being enforced; features like sync may now
        // be disabled by policy, and the rest of the sign-in flow can be resumed.
        doSignIn();
    }

    private void doSignIn() {
        Log.d(TAG, "Committing the sign-in process now");
        assert mSignInAccount != null;

        // Cache the signed-in account name.
        ChromeSigninController.get(mContext).setSignedInAccountName(mSignInAccount.name);

        // Tell the native side that sign-in has completed.
        // This will trigger NOTIFICATION_GOOGLE_SIGNIN_SUCCESSFUL.
        nativeOnSignInCompleted(mNativeSigninManagerAndroid, mSignInAccount.name);

        // Register for invalidations.
        InvalidationController invalidationController = InvalidationController.get(mContext);
        invalidationController.setRegisteredTypes(mSignInAccount, true, new HashSet<ModelType>());

        // Sign-in to sync.
        ProfileSyncService profileSyncService = ProfileSyncService.get(mContext);
        if (SyncStatusHelper.get(mContext).isSyncEnabled(mSignInAccount) &&
                !profileSyncService.hasSyncSetupCompleted()) {
            profileSyncService.setSetupInProgress(true);
            profileSyncService.syncSignIn();
        }

        if (mSignInObserver != null)
            mSignInObserver.onSigninComplete();

        // All done, cleanup.
        Log.d(TAG, "Signin done");
        mSignInActivity = null;
        mSignInAccount = null;
        mSignInObserver = null;
        notifySignInAllowedChanged();
    }

    /**
     * Signs out of Chrome.
     * <p/>
     * This method clears the signed-in username, stops sync and sends out a
     * sign-out notification on the native side.
     *
     * @param activity If not null then a progress dialog is shown over the activity until signout
     * completes, in case the account had management enabled. The activity must be valid until the
     * callback is invoked.
     * @param callback Will be invoked after signout completes, if not null.
     */
    public void signOut(Activity activity, Runnable callback) {
        mSignOutCallback = callback;

        boolean wipeData = getManagementDomain() != null;
        Log.d(TAG, "Signing out, wipe data? " + wipeData);

        ChromeSigninController.get(mContext).clearSignedInUser();
        ProfileSyncService.get(mContext).signOut();
        nativeSignOut(mNativeSigninManagerAndroid);

        if (wipeData) {
            wipeProfileData(activity);
        } else {
            onSignOutDone();
        }
    }

    /**
     * Returns the management domain if the signed in account is managed, otherwise returns null.
     */
    public String getManagementDomain() {
        return nativeGetManagementDomain(mNativeSigninManagerAndroid);
    }

    public void logInSignedInUser() {
        nativeLogInSignedInUser(mNativeSigninManagerAndroid);
    }

    private void cancelSignIn() {
        if (mSignInObserver != null)
            mSignInObserver.onSigninCancelled();
        mSignInActivity = null;
        mSignInObserver = null;
        mSignInAccount = null;
        notifySignInAllowedChanged();
    }

    private void wipeProfileData(Activity activity) {
        if (activity != null) {
            // We don't have progress update, so this takes an indeterminate amount of time.
            boolean indeterminate = true;
            // This dialog is not cancelable by the user.
            boolean cancelable = false;
            mSignOutProgressDialog = ProgressDialog.show(
                activity,
                activity.getString(R.string.wiping_profile_data_title),
                activity.getString(R.string.wiping_profile_data_message),
                indeterminate, cancelable);
        }
        // This will call back to onProfileDataWiped().
        nativeWipeProfileData(mNativeSigninManagerAndroid);
    }

    @CalledByNative
    private void onProfileDataWiped() {
        if (mSignOutProgressDialog != null && mSignOutProgressDialog.isShowing())
            mSignOutProgressDialog.dismiss();
        onSignOutDone();
    }

    private void onSignOutDone() {
        if (mSignOutCallback != null) {
            new Handler().post(mSignOutCallback);
            mSignOutCallback = null;
        }
    }

    // Native methods.
    private native int nativeInit();
    private native boolean nativeShouldLoadPolicyForUser(String username);
    private native void nativeCheckPolicyBeforeSignIn(
            int nativeSigninManagerAndroid, String username);
    private native void nativeFetchPolicyBeforeSignIn(int nativeSigninManagerAndroid);
    private native void nativeOnSignInCompleted(int nativeSigninManagerAndroid, String username);
    private native void nativeSignOut(int nativeSigninManagerAndroid);
    private native String nativeGetManagementDomain(int nativeSigninManagerAndroid);
    private native void nativeWipeProfileData(int nativeSigninManagerAndroid);
    private native void nativeLogInSignedInUser(int nativeSigninManagerAndroid);
}