/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko.sync.repositories.android;

import java.util.ArrayList;
import java.util.List;

import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserContract.DeletedColumns;
import org.mozilla.gecko.db.BrowserContract.DeletedPasswords;
import org.mozilla.gecko.db.BrowserContract.Passwords;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.repositories.InactiveSessionException;
import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.RecordFilter;
import org.mozilla.gecko.sync.repositories.Repository;
import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
import org.mozilla.gecko.sync.repositories.android.RepoUtils.QueryHelper;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
import org.mozilla.gecko.sync.repositories.domain.Record;

import android.content.ContentProviderClient;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;

public class PasswordsRepositorySession extends
    StoreTrackingRepositorySession {

  public static class PasswordsRepository extends Repository {
    @Override
    public void createSession(RepositorySessionCreationDelegate delegate,
        Context context) {
      PasswordsRepositorySession session = new PasswordsRepositorySession(PasswordsRepository.this, context);
      final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate();
      deferredCreationDelegate.onSessionCreated(session);
    }
  }

  private static final String LOG_TAG = "PasswordsRepoSession";
  private static String COLLECTION = "passwords";

  private RepoUtils.QueryHelper passwordsHelper;
  private RepoUtils.QueryHelper deletedPasswordsHelper;
  private ContentProviderClient passwordsProvider;

  private final Context context;

  public PasswordsRepositorySession(Repository repository, Context context) {
    super(repository);
    this.context = context;
    this.passwordsHelper        = new QueryHelper(context, BrowserContractHelpers.PASSWORDS_CONTENT_URI, LOG_TAG);
    this.deletedPasswordsHelper = new QueryHelper(context, BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, LOG_TAG);
    this.passwordsProvider      = context.getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI);
  }

  private static final String[] GUID_COLS = new String[] { Passwords.GUID };
  private static final String[] DELETED_GUID_COLS = new String[] { DeletedColumns.GUID };

  private static final String WHERE_GUID_IS = Passwords.GUID + " = ?";
  private static final String WHERE_DELETED_GUID_IS = DeletedPasswords.GUID + " = ?";

  @Override
  public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) {
    final Runnable guidsSinceRunnable = new Runnable() {
      @Override
      public void run() {

        if (!isActive()) {
          delegate.onGuidsSinceFailed(new InactiveSessionException(null));
          return;
        }

        // Checks succeeded, now get GUIDs.
        final List<String> guids = new ArrayList<String>();
        try {
          Logger.debug(LOG_TAG, "Fetching guidsSince from data table.");
          final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", GUID_COLS, dateModifiedWhere(timestamp), null, null);
          try {
            if (data.moveToFirst()) {
              while (!data.isAfterLast()) {
                guids.add(RepoUtils.getStringFromCursor(data, Passwords.GUID));
                data.moveToNext();
              }
            }
          } finally {
            data.close();
          }

          // Fetch guids from deleted table.
          Logger.debug(LOG_TAG, "Fetching guidsSince from deleted table.");
          final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", DELETED_GUID_COLS, dateModifiedWhereDeleted(timestamp), null, null);
          try {
            if (deleted.moveToFirst()) {
              while (!deleted.isAfterLast()) {
                guids.add(RepoUtils.getStringFromCursor(deleted, DeletedColumns.GUID));
                deleted.moveToNext();
              }
            }
          } finally {
            deleted.close();
          }
        } catch (Exception e) {
          Logger.error(LOG_TAG, "Exception in fetch.");
          delegate.onGuidsSinceFailed(e);
          return;
        }
        String[] guidStrings = new String[guids.size()];
        delegate.onGuidsSinceSucceeded(guids.toArray(guidStrings));
      }
    };

    delegateQueue.execute(guidsSinceRunnable);
  }

  @Override
  public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) {
    final RecordFilter filter = this.storeTracker.getFilter();
    final Runnable fetchSinceRunnable = new Runnable() {
      @Override
      public void run() {
        if (!isActive()) {
          delegate.onFetchFailed(new InactiveSessionException(null), null);
          return;
        }

        final long end = now();
        try {
          // Fetch from data table.
          Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetchSince",
                                                  getAllColumns(),
                                                  dateModifiedWhere(timestamp),
                                                  null, null);
          if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
            return;
          }

          // Fetch from deleted table.
          Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetchSince",
                                                            getAllDeletedColumns(),
                                                            dateModifiedWhereDeleted(timestamp),
                                                            null, null);
          if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
            return;
          }

          // Success!
          try {
            delegate.onFetchCompleted(end);
          } catch (Exception e) {
            Logger.error(LOG_TAG, "Delegate fetch completed callback failed.", e);
            // Don't call failure callback.
            return;
          }
        } catch (Exception e) {
          Logger.error(LOG_TAG, "Exception in fetch.");
          delegate.onFetchFailed(e, null);
        }
      }
    };

    delegateQueue.execute(fetchSinceRunnable);
  }

  @Override
  public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) {
    if (guids == null || guids.length < 1) {
      Logger.error(LOG_TAG, "No guids to be fetched.");
      final long end = now();
      delegateQueue.execute(new Runnable() {
        @Override
        public void run() {
          delegate.onFetchCompleted(end);
        }
      });
      return;
    }

    // Checks succeeded, now fetch.
    final RecordFilter filter = this.storeTracker.getFilter();
    final Runnable fetchRunnable = new Runnable() {
      @Override
      public void run() {
        if (!isActive()) {
          delegate.onFetchFailed(new InactiveSessionException(null), null);
          return;
        }

        final long end = now();
        final String where = RepoUtils.computeSQLInClause(guids.length, "guid");
        Logger.trace(LOG_TAG, "Fetch guids where: " + where);

        try {
          // Fetch records from data table.
          Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetch",
                                                  getAllColumns(),
                                                  where, guids, null);
          if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
            return;
          }

          // Fetch records from deleted table.
          Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetch",
                                                            getAllDeletedColumns(),
                                                            where, guids, null);
          if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
            return;
          }

          delegate.onFetchCompleted(end);

        } catch (Exception e) {
          Logger.error(LOG_TAG, "Exception in fetch.");
          delegate.onFetchFailed(e, null);
        }
      }
    };

    delegateQueue.execute(fetchRunnable);
  }

  @Override
  public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
    fetchSince(0, delegate);
  }

  @Override
  public void store(final Record record) throws NoStoreDelegateException {
    if (delegate == null) {
      Logger.error(LOG_TAG, "No store delegate.");
      throw new NoStoreDelegateException();
    }
    if (record == null) {
      Logger.error(LOG_TAG, "Record sent to store was null.");
      throw new IllegalArgumentException("Null record passed to PasswordsRepositorySession.store().");
    }
    if (!(record instanceof PasswordRecord)) {
      Logger.error(LOG_TAG, "Can't store anything but a PasswordRecord.");
      throw new IllegalArgumentException("Non-PasswordRecord passed to PasswordsRepositorySession.store().");
    }

    final PasswordRecord remoteRecord = (PasswordRecord) record;

    final Runnable storeRunnable = new Runnable() {
      @Override
      public void run() {
        if (!isActive()) {
          Logger.warn(LOG_TAG, "RepositorySession is inactive. Store failing.");
          delegate.onRecordStoreFailed(new InactiveSessionException(null), record.guid);
          return;
        }

        final String guid = remoteRecord.guid;
        if (guid == null) {
          delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."), record.guid);
          return;
        }

        PasswordRecord existingRecord;
        try {
          existingRecord = retrieveByGUID(guid);
        } catch (NullCursorException e) {
          // Indicates a serious problem.
          delegate.onRecordStoreFailed(e, record.guid);
          return;
        } catch (RemoteException e) {
          delegate.onRecordStoreFailed(e, record.guid);
          return;
        }

        long lastLocalRetrieval  = 0;      // lastSyncTimestamp?
        long lastRemoteRetrieval = 0;      // TODO: adjust for clock skew.
        boolean remotelyModified = remoteRecord.lastModified > lastRemoteRetrieval;

        // Check deleted state first.
        if (remoteRecord.deleted) {
          if (existingRecord == null) {
            // Do nothing, record does not exist anyways.
            Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " is deleted, and no local version.");
            return;
          }

          if (existingRecord.deleted) {
            // Record is already tracked as deleted. Delete from local.
            storeRecordDeletion(existingRecord); // different from ABRepoSess.
            Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " and local are both deleted.");
            return;
          }

          // Which one wins?
          if (!remotelyModified) {
            trace("Ignoring deleted record from the past.");
            return;
          }

          boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
          if (!locallyModified) {
            trace("Remote modified, local not. Deleting.");
            storeRecordDeletion(remoteRecord);
            return;
          }

          trace("Both local and remote records have been modified.");
          if (remoteRecord.lastModified > existingRecord.lastModified) {
            trace("Remote is newer, and deleted. Deleting local.");
            storeRecordDeletion(remoteRecord);
            return;
          }

          trace("Remote is older, local is not deleted. Ignoring.");
          if (!locallyModified) {
            Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!");
            // Ensure that this is tracked for upload.
          }
          return;
        }
        // End deletion logic.

        // Now we're processing a non-deleted incoming record.
        if (existingRecord == null) {
          trace("Looking up match for record " + remoteRecord.guid);
          try {
            existingRecord = findExistingRecord(remoteRecord);
          } catch (RemoteException e) {
            Logger.error(LOG_TAG, "Remote exception in findExistingRecord.");
            delegate.onRecordStoreFailed(e, record.guid);
          } catch (NullCursorException e) {
            Logger.error(LOG_TAG, "Null cursor in findExistingRecord.");
            delegate.onRecordStoreFailed(e, record.guid);
          }
        }

        if (existingRecord == null) {
          // The record is new.
          trace("No match. Inserting.");
          Logger.debug(LOG_TAG, "Didn't find matching record. Inserting.");
          Record inserted = null;
          try {
            inserted = insert(remoteRecord);
          } catch (RemoteException e) {
            Logger.debug(LOG_TAG, "Record insert caused a RemoteException.");
            delegate.onRecordStoreFailed(e, record.guid);
            return;
          }
          trackRecord(inserted);
          delegate.onRecordStoreSucceeded(inserted.guid);
          return;
        }

        // We found a local dupe.
        trace("Incoming record " + remoteRecord.guid + " dupes to local record " + existingRecord.guid);
        Logger.debug(LOG_TAG, "remote " + remoteRecord.guid + " dupes to " + existingRecord.guid);
        Record toStore = reconcileRecords(remoteRecord, existingRecord, lastRemoteRetrieval, lastLocalRetrieval);

        if (toStore == null) {
          Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record.");
          return;
        }

        // TODO: pass in timestamps?
        Logger.debug(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid);
        Record replaced = null;
        try {
          replaced = replace(existingRecord, toStore);
        } catch (RemoteException e) {
          Logger.debug(LOG_TAG, "Record replace caused a RemoteException.");
          delegate.onRecordStoreFailed(e, record.guid);
          return;
        }

        // Note that we don't track records here; deciding that is the job
        // of reconcileRecords.
        Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
                              "(" + replaced.androidID + ")");
        delegate.onRecordStoreSucceeded(record.guid);
        return;
      }
    };
    storeWorkQueue.execute(storeRunnable);
  }

  @Override
  public void wipe(final RepositorySessionWipeDelegate delegate) {
    Logger.info(LOG_TAG, "Wiping " + BrowserContractHelpers.PASSWORDS_CONTENT_URI + ", " + BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI);

    Runnable wipeRunnable = new Runnable() {
      @Override
      public void run() {
        if (!isActive()) {
          delegate.onWipeFailed(new InactiveSessionException(null));
          return;
        }

        // Wipe both data and deleted.
        try {
          context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null);
          context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null);
        } catch (Exception e) {
          delegate.onWipeFailed(e);
          return;
        }
        delegate.onWipeSucceeded();
      }
    };
    storeWorkQueue.execute(wipeRunnable);
  }

  @Override
  public void abort() {
    passwordsProvider.release();
    super.abort();
  }

  @Override
  public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
    passwordsProvider.release();
    super.finish(delegate);
  }

  public void deleteGUID(String guid) throws RemoteException {
    final String[] args = new String[] { guid };

    int deleted = passwordsProvider.delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, WHERE_GUID_IS, args) +
                  passwordsProvider.delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, WHERE_DELETED_GUID_IS, args);
    if (deleted == 1) {
      return;
    }
    Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
  }

  /**
   * Insert record and return the record with its updated androidId set.
   *
   * @param record the record to insert.
   * @return updated record.
   * @throws RemoteException
   */
  public PasswordRecord insert(PasswordRecord record) throws RemoteException {
    record.timePasswordChanged = now();
    // TODO: are these necessary for Fennec autocomplete?
    // record.timesUsed = 1;
    // record.timeLastUsed = now();
    ContentValues cv = getContentValues(record);
    Uri insertedUri = passwordsProvider.insert(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv);
    if (insertedUri == null) {
      throw new RemoteException(); // Not much to be done here, save throw.
    }
    record.androidID = ContentUris.parseId(insertedUri);
    return record;
  }

  public Record replace(Record origRecord, Record newRecord) throws RemoteException {
    PasswordRecord newPasswordRecord = (PasswordRecord) newRecord;
    PasswordRecord origPasswordRecord = (PasswordRecord) origRecord;
    propagateTimes(newPasswordRecord, origPasswordRecord);
    ContentValues cv = getContentValues(newPasswordRecord);

    final String[] args = new String[] { origRecord.guid };

    int updated = context.getContentResolver().update(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv, WHERE_GUID_IS, args);
    if (updated != 1) {
      Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + origPasswordRecord.guid);
    }
    return newRecord;
  }

  // When replacing a record, propagate the times.
  private static void propagateTimes(PasswordRecord toRecord, PasswordRecord fromRecord) {
    toRecord.timePasswordChanged = now();
    toRecord.timeCreated  = fromRecord.timeCreated;
    toRecord.timeLastUsed = fromRecord.timeLastUsed;
    toRecord.timesUsed    = fromRecord.timesUsed;
  }

  private static String[] getAllColumns() {
    return BrowserContractHelpers.PasswordColumns;
  }

  private static String[] getAllDeletedColumns() {
    return BrowserContractHelpers.DeletedColumns;
  }

  /**
   * Constructs the DB query string for entry age for deleted records.
   *
   * @param timestamp
   * @return String DB query string for dates to fetch.
   */
  private static String dateModifiedWhereDeleted(long timestamp) {
    return DeletedColumns.TIME_DELETED + " >= " + Long.toString(timestamp);
  }

  /**
   * Constructs the DB query string for entry age for (undeleted) records.
   *
   * @param timestamp
   * @return String DB query string for dates to fetch.
   */
  private static String dateModifiedWhere(long timestamp) {
    return Passwords.TIME_PASSWORD_CHANGED + " >= " + Long.toString(timestamp);
  }


  /**
   * Fetch from the cursor with the given parameters, invoking
   * delegate callbacks and closing the cursor.
   * Returns true on success, false if failure was signaled.
   *
   * @param cursor
            fetch* cursor.
   * @param deleted
   *        true if using deleted table, false when using data table.
   * @param delegate
   *        FetchRecordsDelegate to process records.
   */
  private static boolean fetchAndCloseCursorDeleted(final Cursor cursor,
                                                    final boolean deleted,
                                                    final RecordFilter filter,
                                                    final RepositorySessionFetchRecordsDelegate delegate) {
    if (cursor == null) {
      return true;
    }

    try {
      while (cursor.moveToNext()) {
        Record r = deleted ? deletedPasswordRecordFromCursor(cursor) : passwordRecordFromCursor(cursor);
        if (r != null) {
          if (filter == null || !filter.excludeRecord(r)) {
            Logger.debug(LOG_TAG, "Processing record " + r.guid);
            delegate.onFetchedRecord(r);
          } else {
            Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid);
          }
        }
      }
    } catch (Exception e) {
      Logger.error(LOG_TAG, "Exception in fetch.");
      delegate.onFetchFailed(e, null);
      return false;
    } finally {
      cursor.close();
    }

    return true;
  }

  private PasswordRecord retrieveByGUID(String guid) throws NullCursorException, RemoteException {
    final String[] guidArg = new String[] { guid };

    // Check data table.
    final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".store", BrowserContractHelpers.PasswordColumns, WHERE_GUID_IS, guidArg, null);
    try {
      if (data.moveToFirst()) {
        return passwordRecordFromCursor(data);
      }
    } finally {
      data.close();
    }

    // Check deleted table.
    final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".retrieveByGuid", BrowserContractHelpers.DeletedColumns, WHERE_DELETED_GUID_IS, guidArg, null);
    try {
      if (deleted.moveToFirst()) {
        return deletedPasswordRecordFromCursor(deleted);
      }
    } finally {
      deleted.close();
    }

    return null;
  }

  private static final String WHERE_RECORD_DATA =
    Passwords.HOSTNAME        + " = ? AND " +
    Passwords.HTTP_REALM      + " = ? AND " +
    Passwords.FORM_SUBMIT_URL + " = ? AND " +
    Passwords.USERNAME_FIELD  + " = ? AND " +
    Passwords.PASSWORD_FIELD  + " = ?";

  private PasswordRecord findExistingRecord(PasswordRecord record) throws NullCursorException, RemoteException {
    PasswordRecord foundRecord = null;
    Cursor cursor = null;
    // Only check the data table.
    // We can't encrypt username directly for query, so run a more general query and then filter.
    final String[] whereArgs = new String[] {
      record.hostname,
      record.httpRealm,
      record.formSubmitURL,
      record.usernameField,
      record.passwordField
    };

    try {
      cursor = passwordsHelper.safeQuery(passwordsProvider, ".findRecord", getAllColumns(), WHERE_RECORD_DATA, whereArgs, null);
      while (cursor.moveToNext()) {
        foundRecord = passwordRecordFromCursor(cursor);

        // We don't directly query for username because the
        // username/password values are encrypted in the db.
        // We don't have the keys for encrypting our query,
        // so we run a more general query and then filter
        // the returned records for a matching username.
        Logger.pii(LOG_TAG, "Checking incoming [" + record.encryptedUsername + "] to [" + foundRecord.encryptedUsername + "]");
        if (record.encryptedUsername.equals(foundRecord.encryptedUsername)) {
          Logger.trace(LOG_TAG, "Found matching record: " + foundRecord.guid);
          return foundRecord;
        }
      }
    } finally {
      if (cursor != null) {
        cursor.close();
      }
    }
    Logger.debug(LOG_TAG, "No matching records, returning null.");
    return null;
  }

  private void storeRecordDeletion(Record record) {
    try {
      deleteGUID(record.guid);
    } catch (RemoteException e) {
      Logger.error(LOG_TAG, "RemoteException in password delete.");
      delegate.onRecordStoreFailed(e, record.guid);
      return;
    }
    delegate.onRecordStoreSucceeded(record.guid);
  }

  /**
   * Make a PasswordRecord from a Cursor.
   * @param cur
   *        Cursor from query.
   * @param deleted
   *        true if creating a deleted Record, false if otherwise.
   * @return
   *        PasswordRecord populated from Cursor.
   */
  private static PasswordRecord passwordRecordFromCursor(Cursor cur) {
    if (cur.isAfterLast()) {
      return null;
    }
    String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.GUID);
    long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);

    PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, false);
    rec.id = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ID);
    rec.hostname = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HOSTNAME);
    rec.httpRealm = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HTTP_REALM);
    rec.formSubmitURL = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.FORM_SUBMIT_URL);
    rec.usernameField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.USERNAME_FIELD);
    rec.passwordField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.PASSWORD_FIELD);
    rec.encType = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENC_TYPE);

    // TODO decryption of username/password here (Bug 711636)
    rec.encryptedUsername = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_USERNAME);
    rec.encryptedPassword = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_PASSWORD);

    rec.timeCreated = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_CREATED);
    rec.timeLastUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_LAST_USED);
    rec.timePasswordChanged = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);
    rec.timesUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIMES_USED);
    return rec;
  }

  private static PasswordRecord deletedPasswordRecordFromCursor(Cursor cur) {
    if (cur.isAfterLast()) {
      return null;
    }
    String guid = RepoUtils.getStringFromCursor(cur, DeletedColumns.GUID);
    long lastModified = RepoUtils.getLongFromCursor(cur, DeletedColumns.TIME_DELETED);
    PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, true);
    rec.androidID = RepoUtils.getLongFromCursor(cur, DeletedColumns.ID);
    return rec;
  }

  private static ContentValues getContentValues(Record record) {
    PasswordRecord rec = (PasswordRecord) record;

    ContentValues cv = new ContentValues();
    cv.put(BrowserContract.Passwords.GUID,            rec.guid);
    cv.put(BrowserContract.Passwords.HOSTNAME,        rec.hostname);
    cv.put(BrowserContract.Passwords.HTTP_REALM,      rec.httpRealm);
    cv.put(BrowserContract.Passwords.FORM_SUBMIT_URL, rec.formSubmitURL);
    cv.put(BrowserContract.Passwords.USERNAME_FIELD,  rec.usernameField);
    cv.put(BrowserContract.Passwords.PASSWORD_FIELD,  rec.passwordField);

    // TODO Do encryption of username/password here. Bug 711636
    cv.put(BrowserContract.Passwords.ENC_TYPE,           rec.encType);
    cv.put(BrowserContract.Passwords.ENCRYPTED_USERNAME, rec.encryptedUsername);
    cv.put(BrowserContract.Passwords.ENCRYPTED_PASSWORD, rec.encryptedPassword);

    cv.put(BrowserContract.Passwords.TIME_CREATED,          rec.timeCreated);
    cv.put(BrowserContract.Passwords.TIME_LAST_USED,        rec.timeLastUsed);
    cv.put(BrowserContract.Passwords.TIME_PASSWORD_CHANGED, rec.timePasswordChanged);
    cv.put(BrowserContract.Passwords.TIMES_USED,            rec.timesUsed);
    return cv;
  }
}
