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

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.net.http.AndroidHttpClient;
import android.os.Bundle;
import android.util.Log;
import android.webkit.CookieSyncManager;
import android.webkit.WebView;
import android.webkit.WebViewClient;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.util.EntityUtils;

public class GoogleAccountLogin implements Runnable,
        AccountManagerCallback<Bundle>, OnCancelListener {

    private static final String LOGTAG = "BrowserLogin";

    // Url for issuing the uber token.
    private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse(
            "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false");
    // Url for signing into a particular service.
    private static final Uri TOKEN_AUTH_URL = Uri.parse(
            "https://www.google.com/accounts/TokenAuth");
    // Google account type
    private static final String GOOGLE = "com.google";
    // Last auto login time
    public static final String PREF_AUTOLOGIN_TIME = "last_autologin_time";

    private final Activity mActivity;
    private final Account mAccount;
    private final WebView mWebView;
    private Runnable mRunnable;
    private ProgressDialog mProgressDialog;

    // SID and LSID retrieval process.
    private String mSid;
    private String mLsid;
    private int mState;  // {NONE(0), SID(1), LSID(2)}
    private boolean mTokensInvalidated;
    private String mUserAgent;

    private GoogleAccountLogin(Activity activity, Account account,
            Runnable runnable) {
        mActivity = activity;
        mAccount = account;
        mWebView = new WebView(mActivity);
        mRunnable = runnable;
        mUserAgent = mWebView.getSettings().getUserAgentString();

        // XXX: Doing pre-login causes onResume to skip calling
        // resumeWebViewTimers. So to avoid problems with timers not running, we
        // duplicate the work here using the off-screen WebView.
        CookieSyncManager.getInstance().startSync();
        WebViewTimersControl.getInstance().onBrowserActivityResume(mWebView);

        mWebView.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                return false;
            }
            @Override
            public void onPageFinished(WebView view, String url) {
                done();
            }
        });
    }

    private void saveLoginTime() {
        Editor ed = BrowserSettings.getInstance().getPreferences().edit();
        ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis());
        ed.apply();
    }

    // Runnable
    @Override
    public void run() {
        String url = ISSUE_AUTH_TOKEN_URL.buildUpon()
                .appendQueryParameter("SID", mSid)
                .appendQueryParameter("LSID", mLsid)
                .build().toString();
        // Intentionally not using Proxy.
        AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
        HttpPost request = new HttpPost(url);

        String result = null;
        try {
            HttpResponse response = client.execute(request);
            int status = response.getStatusLine().getStatusCode();
            if (status != HttpStatus.SC_OK) {
                Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url "
                      + status + ": "
                      + response.getStatusLine().getReasonPhrase());
                // Invalidate the tokens once just in case the 403 was for other
                // reasons.
                if (status == HttpStatus.SC_FORBIDDEN && !mTokensInvalidated) {
                    Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens...");
                    // Need to regenerate the auth tokens and try again.
                    invalidateTokens();
                    // XXX: Do not touch any more member variables from this
                    // thread as a second thread will handle the next login
                    // attempt.
                    return;
                }
                done();
                return;
            }
            HttpEntity entity = response.getEntity();
            if (entity == null) {
                Log.d(LOGTAG, "LOGIN_FAIL: Null entity in response");
                done();
                return;
            }
            result = EntityUtils.toString(entity, "UTF-8");
        } catch (Exception e) {
            Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e);
            request.abort();
            done();
            return;
        } finally {
            client.close();
        }
        final String newUrl = TOKEN_AUTH_URL.buildUpon()
                .appendQueryParameter("source", "android-browser")
                .appendQueryParameter("auth", result)
                .appendQueryParameter("continue",
                        BrowserSettings.getFactoryResetHomeUrl(mActivity))
                .build().toString();
        mActivity.runOnUiThread(new Runnable() {
            @Override public void run() {
                // Check mRunnable in case the request has been canceled.  This
                // is most likely not necessary as run() is the only non-UI
                // thread that calls done() but I am paranoid.
                synchronized (GoogleAccountLogin.this) {
                    if (mRunnable == null) {
                        return;
                    }
                    mWebView.loadUrl(newUrl);
                }
            }
        });
    }

    private void invalidateTokens() {
        AccountManager am = AccountManager.get(mActivity);
        am.invalidateAuthToken(GOOGLE, mSid);
        am.invalidateAuthToken(GOOGLE, mLsid);
        mTokensInvalidated = true;
        mState = 1;  // SID
        am.getAuthToken(mAccount, "SID", null, mActivity, this, null);
    }

    // AccountManager callbacks.
    @Override
    public void run(AccountManagerFuture<Bundle> value) {
        try {
            String id = value.getResult().getString(
                    AccountManager.KEY_AUTHTOKEN);
            switch (mState) {
                default:
                case 0:
                    throw new IllegalStateException(
                            "Impossible to get into this state");
                case 1:
                    mSid = id;
                    mState = 2;  // LSID
                    AccountManager.get(mActivity).getAuthToken(
                            mAccount, "LSID", null, mActivity, this, null);
                    break;
                case 2:
                    mLsid = id;
                    new Thread(this).start();
                    break;
            }
        } catch (Exception e) {
            Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e);
            // For all exceptions load the original signin page.
            // TODO: toast login failed?
            done();
        }
    }

    // Start the login process if auto-login is enabled and the user is not
    // already logged in.
    public static void startLoginIfNeeded(Activity activity,
            Runnable runnable) {
        // Already logged in?
        if (isLoggedIn()) {
            runnable.run();
            return;
        }

        // No account found?
        Account[] accounts = getAccounts(activity);
        if (accounts == null || accounts.length == 0) {
            runnable.run();
            return;
        }

        GoogleAccountLogin login =
                new GoogleAccountLogin(activity, accounts[0], runnable);
        login.startLogin();
    }

    private void startLogin() {
        saveLoginTime();
        mProgressDialog = ProgressDialog.show(mActivity,
                mActivity.getString(R.string.pref_autologin_title),
                mActivity.getString(R.string.pref_autologin_progress,
                                    mAccount.name),
                true /* indeterminate */,
                true /* cancelable */,
                this);
        mState = 1;  // SID
        AccountManager.get(mActivity).getAuthToken(
                mAccount, "SID", null, mActivity, this, null);
    }

    private static Account[] getAccounts(Context ctx) {
        return AccountManager.get(ctx).getAccountsByType(GOOGLE);
    }

    // Checks if we already did pre-login.
    private static boolean isLoggedIn() {
        // See if we last logged in less than a week ago.
        long lastLogin = BrowserSettings.getInstance().getPreferences()
                .getLong(PREF_AUTOLOGIN_TIME, -1);
        if (lastLogin == -1) {
            return false;
        }
        return true;
    }

    // Used to indicate that the Browser should continue loading the main page.
    // This can happen on success, error, or timeout.
    private synchronized void done() {
        if (mRunnable != null) {
            Log.d(LOGTAG, "Finished login attempt for " + mAccount.name);
            mActivity.runOnUiThread(mRunnable);

            try {
                mProgressDialog.dismiss();
            } catch (Exception e) {
                // TODO: Switch to a managed dialog solution (DialogFragment?)
                // Also refactor this class, it doesn't
                // play nice with the activity lifecycle, leading to issues
                // with the dialog it manages
                Log.w(LOGTAG, "Failed to dismiss mProgressDialog: " + e.getMessage());
            }
            mRunnable = null;
            mActivity.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mWebView.destroy();
                }
            });
        }
    }

    // Called by the progress dialog on startup.
    public void onCancel(DialogInterface unused) {
        done();
    }

}