/*
 * Copyright 2014 Google Inc. 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.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.samples.apps.iosched.util;

import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;

import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.GoogleAuthUtil;
import com.google.android.gms.auth.GooglePlayServicesAvailabilityException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.common.Scopes;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.plus.People;
import com.google.android.gms.plus.Plus;
import com.google.android.gms.plus.model.people.Person;
import com.google.android.gms.plus.model.people.PersonBuffer;
import com.google.samples.apps.iosched.settings.SettingsUtils;

import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.google.samples.apps.iosched.util.LogUtils.LOGD;
import static com.google.samples.apps.iosched.util.LogUtils.LOGE;
import static com.google.samples.apps.iosched.util.LogUtils.LOGW;
import static com.google.samples.apps.iosched.util.LogUtils.makeLogTag;

/**
 * This helper handles the UI flow for signing in and authenticating an account. It handles
 * connecting to the Google+ API to fetch profile data (name, cover photo, etc) and
 * also getting the auth token for the necessary scopes. The life of this object is
 * tied to an Activity. Do not attempt to share it across Activities, as unhappiness will
 * result.
 */
public class LoginAndAuthHelper implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, ResultCallback<People.LoadPeopleResult> {

    // Request codes for the UIs that we show
    private static final int REQUEST_AUTHENTICATE = 100;
    private static final int REQUEST_RECOVER_FROM_AUTH_ERROR = 101;
    private static final int REQUEST_RECOVER_FROM_PLAY_SERVICES_ERROR = 102;
    private static final int REQUEST_PLAY_SERVICES_ERROR_DIALOG = 103;

    // Auth scopes we need
    private static final List<String> AUTH_SCOPES = new ArrayList<>(Arrays.asList(
            Scopes.PLUS_LOGIN,
            Scopes.DRIVE_APPFOLDER,
            "https://www.googleapis.com/auth/plus.profile.emails.read"));

    public static final String AUTH_TOKEN_TYPE;

    static {
        // Initialize oauth scope
        StringBuilder sb = new StringBuilder();
        sb.append("oauth2:");
        for (String scope : AUTH_SCOPES) {
            sb.append(scope);
            sb.append(" ");
        }
        AUTH_TOKEN_TYPE = sb.toString().trim();
    }

    private static final String TAG = makeLogTag(LoginAndAuthHelper.class);

    Context mAppContext;

    // Controls whether or not we can show sign-in UI. Starts as true;
    // when sign-in *fails*, we will show the UI only once and set this flag to false.
    // After that, we don't attempt again in order not to annoy the user.
    private static boolean sCanShowSignInUi = true;
    private static boolean sCanShowAuthUi = true;

    // The Activity this object is bound to (we use a weak ref to avoid context leaks)
    WeakReference<Activity> mActivityRef;

    // Callbacks interface we invoke to notify the user of this class of useful events
    WeakReference<Callbacks> mCallbacksRef;

    // Name of the account to log in as (e.g. "foo@example.com")
    String mAccountName;

    // API client to interact with Google services
    private GoogleApiClient mGoogleApiClient;

    // Async task that fetches the token
    GetTokenTask mTokenTask = null;

    // Are we in the started state? Started state is between onStart and onStop.
    boolean mStarted = false;

    // True if we are currently showing UIs to resolve a connection error.
    boolean mResolving = false;


    public interface Callbacks {
        void onPlusInfoLoaded(String accountName);
        void onAuthSuccess(String accountName, boolean newlyAuthenticated);
        void onAuthFailure(String accountName);
    }

    public LoginAndAuthHelper(Activity activity, Callbacks callbacks, String accountName) {
        LOGD(TAG, "Helper created. Account: " + mAccountName);
        mActivityRef = new WeakReference<Activity>(activity);
        mCallbacksRef = new WeakReference<Callbacks>(callbacks);
        mAppContext = activity.getApplicationContext();
        mAccountName = accountName;
        if (SettingsUtils.hasUserRefusedSignIn(activity)) {
            // If we know the user refused sign-in, let's not annoy them.
            sCanShowSignInUi = sCanShowAuthUi = false;
        }
    }

    public boolean isStarted() {
        return mStarted;
    }

    public String getAccountName() {
        return mAccountName;
    }

    private Activity getActivity(String methodName) {
        Activity activity = mActivityRef.get();
        if (activity == null) {
            LOGD(TAG, "Helper lost Activity reference, ignoring (" + methodName + ")");
        }
        return activity;
    }

    public void retryAuthByUserRequest() {
        LOGD(TAG, "Retrying sign-in/auth (user-initiated).");
        if (!mGoogleApiClient.isConnected()) {
            sCanShowAuthUi = sCanShowSignInUi = true;
            SettingsUtils.markUserRefusedSignIn(mAppContext, false);
            mGoogleApiClient.connect();
        } else if (!AccountUtils.hasToken(mAppContext, mAccountName)) {
            sCanShowAuthUi = sCanShowSignInUi = true;
            SettingsUtils.markUserRefusedSignIn(mAppContext, false);
            mTokenTask = new GetTokenTask();
            mTokenTask.execute();
        } else {
            LOGD(TAG, "No need to retry auth: GoogleApiClient is connected and we have auth token.");
        }
    }

    /** Starts the helper. Call this from your Activity's onStart(). */
    public void start() {
        Activity activity = getActivity("start()");
        if (activity == null) {
            return;
        }

        if (mStarted) {
            LOGW(TAG, "Helper already started. Ignoring redundant call.");
            return;
        }

        mStarted = true;
        if (mResolving) {
            // if resolving, don't reconnect the plus client
            LOGD(TAG, "Helper ignoring signal to start because we're resolving a failure.");
            return;
        }
        LOGD(TAG, "Helper starting. Connecting " + mAccountName);
        if (mGoogleApiClient == null) {
            LOGD(TAG, "Creating client.");

            GoogleApiClient.Builder builder = new GoogleApiClient.Builder(activity);
            for (String scope : AUTH_SCOPES) {
                builder.addScope(new Scope(scope));
            }
            mGoogleApiClient = builder.addApi(Plus.API)
                    .addConnectionCallbacks(this)
                    .addOnConnectionFailedListener(this)
                    .setAccountName(mAccountName)
                    .build();
        }
        LOGD(TAG, "Connecting client.");
        mGoogleApiClient.connect();
    }

    // Called when the Google+ client is connected.
    @Override
    public void onConnected(Bundle bundle) {
        Activity activity = getActivity("onConnected()");
        if (activity == null) {
            return;
        }

        LOGD(TAG, "Helper connected, account " + mAccountName);

        // load user's Google+ profile, if we don't have it yet
        if (!AccountUtils.hasPlusInfo(activity, mAccountName)) {
            LOGD(TAG, "We don't have Google+ info for " + mAccountName + " yet, so loading.");
            PendingResult<People.LoadPeopleResult> result = Plus.PeopleApi.load(mGoogleApiClient, "me");
            result.setResultCallback(this);
        } else {
            LOGD(TAG, "No need for Google+ info, we already have it.");
        }

        // try to authenticate, if we don't have a token yet
        if (!AccountUtils.hasToken(activity, mAccountName)) {
            LOGD(TAG, "We don't have auth token for " + mAccountName + " yet, so getting it.");
            mTokenTask = new GetTokenTask();
            mTokenTask.execute();
        } else {
            LOGD(TAG, "No need for auth token, we already have it.");
            reportAuthSuccess(false);
        }
    }

    @Override
    public void onConnectionSuspended(int i) {
        LOGD(TAG, "onConnectionSuspended.");
    }

    /** Stop the helper. Call this from your Activity's onStop(). */
    public void stop() {
        if (!mStarted) {
            LOGW(TAG, "Helper already stopped. Ignoring redundant call.");
            return;
        }

        LOGD(TAG, "Helper stopping.");
        if (mTokenTask != null) {
            LOGD(TAG, "Helper cancelling token task.");
            mTokenTask.cancel(false);
        }
        mStarted = false;
        if (mGoogleApiClient.isConnected()) {
            LOGD(TAG, "Helper disconnecting client.");
            mGoogleApiClient.disconnect();
        }
        mResolving = false;
    }

    // Called when the connection to Google Play Services fails.
    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        Activity activity = getActivity("onConnectionFailed()");
        if (activity == null) {
            return;
        }

        if (connectionResult.hasResolution()) {
            if (sCanShowSignInUi) {
                LOGD(TAG, "onConnectionFailed, with resolution. Attempting to resolve.");
                sCanShowSignInUi = false;
                try {
                    mResolving = true;
                    connectionResult.startResolutionForResult(activity,
                            REQUEST_RECOVER_FROM_PLAY_SERVICES_ERROR);
                } catch (IntentSender.SendIntentException e) {
                    LOGE(TAG, "SendIntentException occurred: " + e.getMessage());
                    e.printStackTrace();
                }
            } else {
                LOGD(TAG, "onConnectionFailed with resolution but sCanShowSignInUi==false.");
                reportAuthFailure();
            }
            return;
        }

        LOGD(TAG, "onConnectionFailed, no resolution.");
        final int errorCode = connectionResult.getErrorCode();
        if (GooglePlayServicesUtil.isUserRecoverableError(errorCode) && sCanShowSignInUi) {
            sCanShowSignInUi = false;
            GooglePlayServicesUtil.getErrorDialog(errorCode, activity,
                    REQUEST_PLAY_SERVICES_ERROR_DIALOG).show();
        } else {
            reportAuthFailure();
        }
    }

    // Called asynchronously -- result of loadPeople() call
    @Override
    public void onResult(People.LoadPeopleResult loadPeopleResult) {
        LOGD(TAG, "onPeopleLoaded, status=" + loadPeopleResult.getStatus().toString());
        if (loadPeopleResult.getStatus().isSuccess()) {
            PersonBuffer personBuffer = loadPeopleResult.getPersonBuffer();
            if (personBuffer != null && personBuffer.getCount() > 0) {
                LOGD(TAG, "Got plus profile for account " + mAccountName);
                Person currentUser = personBuffer.get(0);
                personBuffer.close();

                // Record profile ID, image URL and name
                LOGD(TAG, "Saving plus profile ID: " + currentUser.getId());
                AccountUtils.setPlusProfileId(mAppContext, mAccountName, currentUser.getId());
                String imageUrl = currentUser.getImage().getUrl();
                if (imageUrl != null) {
                    imageUrl = Uri.parse(imageUrl)
                            .buildUpon().appendQueryParameter("sz", "256").build().toString();
                }
                LOGD(TAG, "Saving plus image URL: " + imageUrl);
                AccountUtils.setPlusImageUrl(mAppContext, mAccountName, imageUrl);
                LOGD(TAG, "Saving plus display name: " + currentUser.getDisplayName());
                AccountUtils.setPlusName(mAppContext, mAccountName, currentUser.getDisplayName());
                Person.Cover cover = currentUser.getCover();
                if (cover != null) {
                    Person.Cover.CoverPhoto coverPhoto = cover.getCoverPhoto();
                    if (coverPhoto != null) {
                        LOGD(TAG, "Saving plus cover URL: " + coverPhoto.getUrl());
                        AccountUtils.setPlusCoverUrl(mAppContext, mAccountName, coverPhoto.getUrl());
                    }
                } else {
                    LOGD(TAG, "Profile has no cover.");
                }

                Callbacks callbacks;
                if (null != (callbacks = mCallbacksRef.get())) {
                    callbacks.onPlusInfoLoaded(mAccountName);
                }
            } else {
                LOGE(TAG, "Plus response was empty! Failed to load profile.");
            }
        } else {
            LOGE(TAG, "Failed to load plus proflie, error " + loadPeopleResult.getStatus().getStatusCode());
        }
    }

    /** Handles an Activity result. Call this from your Activity's onActivityResult(). */
    public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
        Activity activity = getActivity("onActivityResult()");
        if (activity == null) {
            return false;
        }

        if (requestCode == REQUEST_AUTHENTICATE ||
                requestCode == REQUEST_RECOVER_FROM_AUTH_ERROR ||
                requestCode == REQUEST_PLAY_SERVICES_ERROR_DIALOG) {

            LOGD(TAG, "onActivityResult, req=" + requestCode + ", result=" + resultCode);
            if (requestCode == REQUEST_RECOVER_FROM_PLAY_SERVICES_ERROR) {
                mResolving = false;
            }

            if (resultCode == Activity.RESULT_OK) {
                if (mGoogleApiClient != null) {
                    LOGD(TAG, "Since activity result was RESULT_OK, reconnecting client.");
                    mGoogleApiClient.connect();
                } else {
                    LOGD(TAG, "Activity result was RESULT_OK, but we have no client to reconnect.");
                }
            } else if (resultCode == Activity.RESULT_CANCELED) {
                LOGD(TAG, "User explicitly cancelled sign-in/auth flow.");
                // Save the refusal so the user isn't annoyed again.
                SettingsUtils.markUserRefusedSignIn(mAppContext, true);
            } else {
                LOGW(TAG, "Failed to recover from a login/auth failure, resultCode=" + resultCode);
            }
            return true;
        }
        return false;
    }

    private void showRecoveryDialog(int statusCode) {
        Activity activity = getActivity("showRecoveryDialog()");
        if (activity == null) {
            return;
        }

        if (sCanShowAuthUi) {
            sCanShowAuthUi = false;
            LOGD(TAG, "Showing recovery dialog for status code " + statusCode);
            final Dialog d = GooglePlayServicesUtil.getErrorDialog(
                    statusCode, activity, REQUEST_RECOVER_FROM_PLAY_SERVICES_ERROR);
            d.show();
        } else {
            LOGD(TAG, "Not showing Play Services recovery dialog because sCanShowSignInUi==false.");
            reportAuthFailure();
        }
    }

    private void showAuthRecoveryFlow(Intent intent) {
        Activity activity = getActivity("showAuthRecoveryFlow()");
        if (activity == null) {
            return;
        }

        if (sCanShowAuthUi) {
            sCanShowAuthUi = false;
            LOGD(TAG, "Starting auth recovery Intent.");
            activity.startActivityForResult(intent, REQUEST_RECOVER_FROM_AUTH_ERROR);
        } else {
            LOGD(TAG, "Not showing auth recovery flow because sCanShowSignInUi==false.");
            reportAuthFailure();
        }
    }

    private void reportAuthSuccess(boolean newlyAuthenticated) {
        LOGD(TAG, "Auth success for account " + mAccountName + ", newlyAuthenticated=" + newlyAuthenticated);
        Callbacks callbacks;
        if (null != (callbacks = mCallbacksRef.get())) {
            callbacks.onAuthSuccess(mAccountName, newlyAuthenticated);
        }
    }

    private void reportAuthFailure() {
        LOGD(TAG, "Auth FAILURE for account " + mAccountName);
        Callbacks callbacks;
        if (null != (callbacks = mCallbacksRef.get())) {
            callbacks.onAuthFailure(mAccountName);
        }
    }

    /** Async task that obtains the auth token. */
    private class GetTokenTask extends AsyncTask<Void, Void, String> {
        public GetTokenTask() {}

        @Override
        protected String doInBackground(Void... params) {
            try {
                if (isCancelled()) {
                    LOGD(TAG, "doInBackground: task cancelled, so giving up on auth.");
                    return null;
                }

                LOGD(TAG, "Starting background auth for " + mAccountName);
                final String token = GoogleAuthUtil
                        .getToken(mAppContext, mAccountName, AUTH_TOKEN_TYPE);

                // Save auth token.
                LOGD(TAG, "Saving token: " + (token == null ? "(null)" : "(length " +
                        token.length() + ")") + " for account "  + mAccountName);
                AccountUtils.setAuthToken(mAppContext, mAccountName, token);
                return token;
            } catch (GooglePlayServicesAvailabilityException e) {
                postShowRecoveryDialog(e.getConnectionStatusCode());
            } catch (UserRecoverableAuthException e) {
                postShowAuthRecoveryFlow(e.getIntent());
            } catch (IOException e) {
                LOGE(TAG, "IOException encountered: " + e.getMessage());
            } catch (GoogleAuthException e) {
                LOGE(TAG, "GoogleAuthException encountered: " + e.getMessage());
            } catch (RuntimeException e) {
                LOGE(TAG, "RuntimeException encountered: " + e.getMessage());
            }
            return null;
        }

        @Override
        protected void onPostExecute(String token) {
            super.onPostExecute(token);

            if (isCancelled()) {
                LOGD(TAG, "Task cancelled, so not reporting auth success.");
            } else if (!mStarted) {
                LOGD(TAG, "Activity not started, so not reporting auth success.");
            } else {
                LOGD(TAG, "GetTokenTask reporting auth success.");
                reportAuthSuccess(true);
            }
        }

        private void postShowRecoveryDialog(final int statusCode) {
            Activity activity = getActivity("postShowRecoveryDialog()");
            if (activity == null) {
                return;
            }

            if (isCancelled()) {
                LOGD(TAG, "Task cancelled, so not showing recovery dialog.");
                return;
            }

            LOGD(TAG, "Requesting display of recovery dialog for status code " + statusCode);
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (mStarted) {
                        showRecoveryDialog(statusCode);
                    } else {
                        LOGE(TAG, "Activity not started, so not showing recovery dialog.");
                    }
                }
            });
        }

        private void postShowAuthRecoveryFlow(final Intent intent) {
            Activity activity = getActivity("postShowAuthRecoveryFlow()");
            if (activity == null) {
                return;
            }

            if (isCancelled()) {
                LOGD(TAG, "Task cancelled, so not showing auth recovery flow.");
                return;
            }

            LOGD(TAG, "Requesting display of auth recovery flow.");
            activity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (mStarted) {
                        showAuthRecoveryFlow(intent);
                    } else {
                        LOGE(TAG, "Activity not started, so not showing auth recovery flow.");
                    }
                }
            });
        }
    }

}
