// Copyright 2012 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.android_webview;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.util.Log;

/**
 * This database is used to support WebView's setHttpAuthUsernamePassword and
 * getHttpAuthUsernamePassword methods, and WebViewDatabase's clearHttpAuthUsernamePassword and
 * hasHttpAuthUsernamePassword methods.
 *
 * While this class is intended to be used as a singleton, this property is not enforced in this
 * layer, primarily for ease of testing. To line up with the classic implementation and behavior,
 * there is no specific handling and reporting when SQL errors occur.
 *
 * Note on thread-safety: As per the classic implementation, most API functions have thread safety
 * provided by the underlying SQLiteDatabase instance. The exception is database opening: this
 * is handled in the dedicated background thread, which also provides a performance gain
 * if triggered early on (e.g. as a side effect of CookieSyncManager.createInstance() call),
 * sufficiently in advance of the first blocking usage of the API.
 */
public class HttpAuthDatabase {

    private static final String LOGTAG = "HttpAuthDatabase";

    private static final int DATABASE_VERSION = 1;

    private SQLiteDatabase mDatabase = null;

    private static final String ID_COL = "_id";

    private static final String[] ID_PROJECTION = new String[] {
        ID_COL
    };

    // column id strings for "httpauth" table
    private static final String HTTPAUTH_TABLE_NAME = "httpauth";
    private static final String HTTPAUTH_HOST_COL = "host";
    private static final String HTTPAUTH_REALM_COL = "realm";
    private static final String HTTPAUTH_USERNAME_COL = "username";
    private static final String HTTPAUTH_PASSWORD_COL = "password";

    /**
     * Initially false until the background thread completes.
     */
    private boolean mInitialized = false;

    private final Object mInitializedLock = new Object();

    /**
     * Create an instance of HttpAuthDatabase for the named file, and kick-off background
     * initialization of that database.
     *
     * @param context the Context to use for opening the database
     * @param databaseFile Name of the file to be initialized.
     */
    public HttpAuthDatabase(final Context context, final String databaseFile) {
        new Thread() {
            @Override
            public void run() {
                initOnBackgroundThread(context, databaseFile);
            }
        }.start();
    }

    /**
     * Initializes the databases and notifies any callers waiting on waitForInit.
     *
     * @param context the Context to use for opening the database
     * @param databaseFile Name of the file to be initialized.
     */
    private void initOnBackgroundThread(Context context, String databaseFile) {
        synchronized (mInitializedLock) {
            if (mInitialized) {
                return;
            }

            initDatabase(context, databaseFile);

            // Thread done, notify.
            mInitialized = true;
            mInitializedLock.notifyAll();
        }
    }

    /**
     * Opens the database, and upgrades it if necessary.
     *
     * @param context the Context to use for opening the database
     * @param databaseFile Name of the file to be initialized.
     */
    private void initDatabase(Context context, String databaseFile) {
        try {
            mDatabase = context.openOrCreateDatabase(databaseFile, 0, null);
        } catch (SQLiteException e) {
            // try again by deleting the old db and create a new one
            if (context.deleteDatabase(databaseFile)) {
                mDatabase = context.openOrCreateDatabase(databaseFile, 0, null);
            }
        }

        if (mDatabase == null) {
            // Not much we can do to recover at this point
            Log.e(LOGTAG, "Unable to open or create " + databaseFile);
            return;
        }

        if (mDatabase.getVersion() != DATABASE_VERSION) {
            mDatabase.beginTransactionNonExclusive();
            try {
                createTable();
                mDatabase.setTransactionSuccessful();
            } finally {
                mDatabase.endTransaction();
            }
        }
    }

    private void createTable() {
        mDatabase.execSQL("CREATE TABLE " + HTTPAUTH_TABLE_NAME
                + " (" + ID_COL + " INTEGER PRIMARY KEY, "
                + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL
                + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, "
                + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE ("
                + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL
                + ") ON CONFLICT REPLACE);");

        mDatabase.setVersion(DATABASE_VERSION);
    }

    /**
     * Waits for the background initialization thread to complete and check the database creation
     * status.
     *
     * @return true if the database was initialized, false otherwise
     */
    private boolean waitForInit() {
        synchronized (mInitializedLock) {
            while (!mInitialized) {
                try {
                    mInitializedLock.wait();
                } catch (InterruptedException e) {
                    Log.e(LOGTAG, "Caught exception while checking initialization", e);
                }
            }
        }
        return mDatabase != null;
    }

    /**
     * Sets the HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, HTTPAUTH_REALM_COL,
     * HTTPAUTH_USERNAME_COL) is unique.
     *
     * @param host the host for the password
     * @param realm the realm for the password
     * @param username the username for the password.
     * @param password the password
     */
    public void setHttpAuthUsernamePassword(String host, String realm, String username,
            String password) {
        if (host == null || realm == null || !waitForInit()) {
            return;
        }

        final ContentValues c = new ContentValues();
        c.put(HTTPAUTH_HOST_COL, host);
        c.put(HTTPAUTH_REALM_COL, realm);
        c.put(HTTPAUTH_USERNAME_COL, username);
        c.put(HTTPAUTH_PASSWORD_COL, password);
        mDatabase.insert(HTTPAUTH_TABLE_NAME, HTTPAUTH_HOST_COL, c);
    }

    /**
     * Retrieves the HTTP authentication username and password for a given host and realm pair. If
     * there are multiple username/password combinations for a host/realm, only the first one will
     * be returned.
     *
     * @param host the host the password applies to
     * @param realm the realm the password applies to
     * @return a String[] if found where String[0] is username (which can be null) and
     *         String[1] is password.  Null is returned if it can't find anything.
     */
    public String[] getHttpAuthUsernamePassword(String host, String realm) {
        if (host == null || realm == null || !waitForInit()) {
            return null;
        }

        final String[] columns = new String[] {
            HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL
        };
        final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND " +
                "(" + HTTPAUTH_REALM_COL + " == ?)";

        String[] ret = null;
        Cursor cursor = null;
        try {
            cursor = mDatabase.query(HTTPAUTH_TABLE_NAME, columns, selection,
                    new String[] { host, realm }, null, null, null);
            if (cursor.moveToFirst()) {
                ret = new String[] {
                        cursor.getString(cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)),
                        cursor.getString(cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)),
                };
            }
        } catch (IllegalStateException e) {
            Log.e(LOGTAG, "getHttpAuthUsernamePassword", e);
        } finally {
            if (cursor != null) cursor.close();
        }
        return ret;
    }

    /**
     * Determines if there are any HTTP authentication passwords saved.
     *
     * @return true if there are passwords saved
     */
    public boolean hasHttpAuthUsernamePassword() {
        if (!waitForInit()) {
            return false;
        }

        Cursor cursor = null;
        boolean ret = false;
        try {
            cursor = mDatabase.query(HTTPAUTH_TABLE_NAME, ID_PROJECTION, null, null, null, null,
                    null);
            ret = cursor.moveToFirst();
        } catch (IllegalStateException e) {
            Log.e(LOGTAG, "hasEntries", e);
        } finally {
            if (cursor != null) cursor.close();
        }
        return ret;
    }

    /**
     * Clears the HTTP authentication password database.
     */
    public void clearHttpAuthUsernamePassword() {
        if (!waitForInit()) {
            return;
        }
        mDatabase.delete(HTTPAUTH_TABLE_NAME, null, null);
    }
}
