/* 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.config;

import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;

/**
 * Migrate Sync preferences between versions.
 * <p>
 * The original preferences were un-versioned; we refer to that as "version 0".
 * The original preferences were stored in three places:
 * <ul>
 * <li>most prefs were kept in per-Sync account Android shared prefs;</li>
 * <li>some prefs were kept in per-App Android shared prefs;</li>
 * <li>some client prefs were kept in the (assumed unique) Android Account.</li>
 * </ul>
 * <p>
 * Post version 0, all preferences are stored in per-Sync account Android shared prefs.
 */
public class ConfigurationMigrator {
  public static final String LOG_TAG = "ConfigMigrator";

  /**
   * Copy and rename preferences.
   *
   * @param from source.
   * @param to sink.
   * @param map map from old preference names to new preference names.
   * @return the number of preferences migrated.
   */
  protected static int copyPreferences(final SharedPreferences from, final Map<String, String> map, final Editor to) {
    int count = 0;

    // SharedPreferences has no way to get a key/value pair without specifying the value type, so we do this instead.
    for (Entry<String, ?> entry : from.getAll().entrySet()) {
      String fromKey = entry.getKey();
      String toKey = map.get(fromKey);
      if (toKey == null) {
        continue;
      }

      Object value = entry.getValue();
      if (value instanceof Boolean) {
         to.putBoolean(toKey, ((Boolean) value).booleanValue());
      } else if (value instanceof Float) {
         to.putFloat(toKey, ((Float) value).floatValue());
      } else if (value instanceof Integer) {
         to.putInt(toKey, ((Integer) value).intValue());
      } else if (value instanceof Long) {
         to.putLong(toKey, ((Long) value).longValue());
      } else if (value instanceof String) {
         to.putString(toKey, (String) value);
      } else {
        // Do nothing -- perhaps SharedPreferences accepts types we don't know about.
      }

      if (Logger.LOG_PERSONAL_INFORMATION) {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + value + ").");
      } else {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
      }
      count += 1;
    }

    return count;
  }

  protected final static String V0_PREF_CLUSTER_URL_IS_STALE = "clusterurlisstale";
  protected final static String V1_PREF_CLUSTER_URL_IS_STALE = V0_PREF_CLUSTER_URL_IS_STALE;
  protected final static String V0_PREF_EARLIEST_NEXT_SYNC = "earliestnextsync";
  protected final static String V1_PREF_EARLIEST_NEXT_SYNC = V0_PREF_EARLIEST_NEXT_SYNC;

  /**
   * Extract version 0 preferences from per-App Android shared prefs and write to version 1 per-Sync account shared prefs.
   *
   * @param from per-App version 0 Android shared prefs.
   * @param to per-Sync account version 1 shared prefs.
   * @return the number of preferences migrated.
   * @throws Exception
   */
  protected static int upgradeGlobals0to1(final SharedPreferences from, final SharedPreferences to) throws Exception {
    Map<String, String> map = new HashMap<String, String>();
    map.put(V0_PREF_CLUSTER_URL_IS_STALE, V1_PREF_CLUSTER_URL_IS_STALE);
    map.put(V0_PREF_EARLIEST_NEXT_SYNC, V1_PREF_EARLIEST_NEXT_SYNC);

    Editor editor = to.edit();
    int count = copyPreferences(from, map, editor);
    if (count > 0) {
      editor.commit();
    }
    return count;
  }

  /**
   * Extract version 1 per-Sync account shared prefs and write to version 0 preferences from per-App Android shared prefs.
   *
   * @param from per-Sync account version 1 shared prefs.
   * @param to per-App version 0 Android shared prefs.
   * @return the number of preferences migrated.
   * @throws Exception
   */
  protected static int downgradeGlobals1to0(final SharedPreferences from, final SharedPreferences to) throws Exception {
    Map<String, String> map = new HashMap<String, String>();
    map.put(V1_PREF_CLUSTER_URL_IS_STALE, V0_PREF_CLUSTER_URL_IS_STALE);
    map.put(V1_PREF_EARLIEST_NEXT_SYNC, V0_PREF_EARLIEST_NEXT_SYNC);

    Editor editor = to.edit();
    int count = copyPreferences(from, map, editor);
    if (count > 0) {
      editor.commit();
    }
    return count;
  }

  protected static final String V0_PREF_ACCOUNT_GUID = "account.guid";
  protected static final String V1_PREF_ACCOUNT_GUID = V0_PREF_ACCOUNT_GUID;
  protected static final String V0_PREF_CLIENT_NAME = "account.clientName";
  protected static final String V1_PREF_CLIENT_NAME = V0_PREF_CLIENT_NAME;
  protected static final String V0_PREF_NUM_CLIENTS = "account.numClients";
  protected static final String V1_PREF_NUM_CLIENTS = V0_PREF_NUM_CLIENTS;

  /**
   * Extract version 0 per-Android account user data and write to version 1 per-Sync account shared prefs.
   *
   * @param accountManager Android account manager.
   * @param account Android account.
   * @param to per-Sync account version 1 shared prefs.
   * @return the number of preferences migrated.
   * @throws Exception
   */
  protected static int upgradeAndroidAccount0to1(final AccountManager accountManager, final Account account, final SharedPreferences to) throws Exception {
    final String V0_PREF_ACCOUNT_GUID = "account.guid";
    final String V1_PREF_ACCOUNT_GUID = V0_PREF_ACCOUNT_GUID;
    final String V0_PREF_CLIENT_NAME = "account.clientName";
    final String V1_PREF_CLIENT_NAME = V0_PREF_CLIENT_NAME;
    final String V0_PREF_NUM_CLIENTS = "account.numClients";
    final String V1_PREF_NUM_CLIENTS = V0_PREF_NUM_CLIENTS;

    String accountGUID = null;
    String clientName = null;
    long numClients = -1;
    try {
      accountGUID = accountManager.getUserData(account, V0_PREF_ACCOUNT_GUID);
    } catch (Exception e) {
      // Do nothing.
    }
    try {
      clientName = accountManager.getUserData(account, V0_PREF_CLIENT_NAME);
    } catch (Exception e) {
      // Do nothing.
    }
    try {
      numClients = Long.parseLong(accountManager.getUserData(account, V0_PREF_NUM_CLIENTS));
    } catch (Exception e) {
      // Do nothing.
    }

    final Editor editor = to.edit();

    int count = 0;
    if (accountGUID != null) {
      final String fromKey = V0_PREF_ACCOUNT_GUID;
      final String toKey = V1_PREF_ACCOUNT_GUID;
      if (Logger.LOG_PERSONAL_INFORMATION) {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + accountGUID + ").");
      } else {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
      }
      editor.putString(toKey, accountGUID);
      count += 1;
    }
    if (clientName != null) {
      final String fromKey = V0_PREF_CLIENT_NAME;
      final String toKey = V1_PREF_CLIENT_NAME;
      if (Logger.LOG_PERSONAL_INFORMATION) {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + clientName + ").");
      } else {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
      }
      editor.putString(toKey, clientName);
      count += 1;
    }
    if (numClients > -1) {
      final String fromKey = V0_PREF_NUM_CLIENTS;
      final String toKey = V1_PREF_NUM_CLIENTS;
      if (Logger.LOG_PERSONAL_INFORMATION) {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "' (" + numClients + ").");
      } else {
        Logger.debug(LOG_TAG, "Migrated '" + fromKey + "' to '" + toKey + "'.");
      }
      editor.putLong(toKey, numClients);
      count += 1;
    }

    if (count > 0) {
      editor.commit();
    }
    return count;
  }

  /**
   * Extract version 1 per-Sync account shared prefs and write to version 0 per-Android account user data.
   *
   * @param from per-Sync account version 1 shared prefs.
   * @param accountManager Android account manager.
   * @param account Android account.
   * @return the number of preferences migrated.
   * @throws Exception
   */
  protected static int downgradeAndroidAccount1to0(final SharedPreferences from, final AccountManager accountManager, final Account account) throws Exception {
    final String accountGUID = from.getString(V1_PREF_ACCOUNT_GUID, null);
    final String clientName = from.getString(V1_PREF_CLIENT_NAME, null);
    final long numClients = from.getLong(V1_PREF_NUM_CLIENTS, -1L);

    int count = 0;
    if (accountGUID != null) {
      Logger.debug(LOG_TAG, "Migrated account GUID.");
      accountManager.setUserData(account, V0_PREF_ACCOUNT_GUID, accountGUID);
      count += 1;
    }
    if (clientName != null) {
      Logger.debug(LOG_TAG, "Migrated client name.");
      accountManager.setUserData(account, V1_PREF_CLIENT_NAME, clientName);
      count += 1;
    }
    if (numClients > -1) {
      Logger.debug(LOG_TAG, "Migrated clients count.");
      accountManager.setUserData(account, V1_PREF_NUM_CLIENTS, new Long(numClients).toString());
      count += 1;
    }
    return count;
  }

  /**
   * Extract version 0 per-Android account user data and write to version 1 per-Sync account shared prefs.
   *
   * @param from per-Sync account version 0 shared prefs.
   * @param to per-Sync account version 1 shared prefs.
   * @return the number of preferences migrated.
   * @throws Exception
   */
  protected static int upgradeShared0to1(final SharedPreferences from, final SharedPreferences to) {
    final Map<String, String> map = new HashMap<String, String>();
    final String[] prefs = new String [] {
        "syncID",
        "clusterURL",
        "enabledEngineNames",

        "metaGlobalLastModified", "metaGlobalServerResponseBody",

        "crypto5KeysLastModified", "crypto5KeysServerResponseBody",

        "serverClientsTimestamp", "serverClientRecordTimestamp",

        "forms.remote", "forms.local", "forms.syncID",
        "tabs.remote", "tabs.local", "tabs.syncID",
        "passwords.remote", "passwords.local", "passwords.syncID",
        "history.remote", "history.local", "history.syncID",
        "bookmarks.remote", "bookmarks.local", "bookmarks.syncID",
    };
    for (String pref : prefs) {
      map.put(pref, pref);
    }

    Editor editor = to.edit();
    int count = copyPreferences(from, map, editor);
    if (count > 0) {
      editor.commit();
    }
    return count;
  }

  /**
   * Extract version 1 per-Sync account shared prefs and write to version 0 per-Android account user data.
   *
   * @param from per-Sync account version 1 shared prefs.
   * @param to per-Sync account version 0 shared prefs.
   * @return the number of preferences migrated.
   * @throws Exception
   */
  protected static int downgradeShared1to0(final SharedPreferences from, final SharedPreferences to) {
    // Strictly a copy, no re-naming, no deletions -- so just invert.
    return upgradeShared0to1(from, to);
  }

  public static void upgrade0to1(final Context context, final AccountManager accountManager, final Account account,
      final String product, final String username, final String serverURL, final String profile) throws Exception {

    final String GLOBAL_SHARED_PREFS = "sync.prefs.global";

    final SharedPreferences globalPrefs = context.getSharedPreferences(GLOBAL_SHARED_PREFS, Utils.SHARED_PREFERENCES_MODE);
    final SharedPreferences accountPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 0);
    final SharedPreferences newPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 1);

    upgradeGlobals0to1(globalPrefs, newPrefs);
    upgradeAndroidAccount0to1(accountManager, account, newPrefs);
    upgradeShared0to1(accountPrefs, newPrefs);
  }

  public static void downgrade1to0(final Context context, final AccountManager accountManager, final Account account,
      final String product, final String username, final String serverURL, final String profile) throws Exception {

    final String GLOBAL_SHARED_PREFS = "sync.prefs.global";

    final SharedPreferences globalPrefs = context.getSharedPreferences(GLOBAL_SHARED_PREFS, Utils.SHARED_PREFERENCES_MODE);
    final SharedPreferences accountPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 0);
    final SharedPreferences oldPrefs = Utils.getSharedPreferences(context, product, username, serverURL, profile, 1);

    downgradeGlobals1to0(oldPrefs, globalPrefs);
    downgradeAndroidAccount1to0(oldPrefs, accountManager, account);
    downgradeShared1to0(oldPrefs, accountPrefs);
  }

  /**
   * Migrate, if necessary, existing prefs to a certain version.
   * <p>
   * Stores current prefs version in Android shared prefs with root
   * "sync.prefs.version", which corresponds to the file
   * "sync.prefs.version.xml".
   *
   * @param desiredVersion
   *          version to finish it.
   * @param context
   * @param accountManager
   * @param account
   * @param product
   * @param username
   * @param serverURL
   * @param profile
   * @throws Exception
   */
  public static void ensurePrefsAreVersion(final long desiredVersion,
      final Context context, final AccountManager accountManager, final Account account,
      final String product, final String username, final String serverURL, final String profile) throws Exception {
    if (desiredVersion < 0 || desiredVersion > SyncConfiguration.CURRENT_PREFS_VERSION) {
      throw new IllegalArgumentException("Cannot migrate to unknown version " + desiredVersion + ".");
    }

    SharedPreferences versionPrefs = context.getSharedPreferences("sync.prefs.version", Utils.SHARED_PREFERENCES_MODE);

    // We default to 0 since clients getting this code for the first time will
    // not have "sync.prefs.version.xml" *at all*, and upgrading when all old
    // data is missing is expected to be safe.
    long currentVersion = versionPrefs.getLong(SyncConfiguration.PREF_PREFS_VERSION, 0);
    if (currentVersion == desiredVersion) {
      Logger.info(LOG_TAG, "Current version (" + currentVersion + ") is desired version; no need to migrate.");
      return;
    }

    if (currentVersion < 0 || currentVersion > SyncConfiguration.CURRENT_PREFS_VERSION) {
      throw new IllegalStateException("Cannot migrate from unknown version " + currentVersion + ".");
    }

    // Now we're down to either version 0 or version 1.
    if (currentVersion == 0 && desiredVersion == 1) {
      Logger.info(LOG_TAG, "Upgrading from version 0 to version 1.");
      upgrade0to1(context, accountManager, account, product, username, serverURL, profile);
    } else if (currentVersion == 1 && desiredVersion == 0) {
      Logger.info(LOG_TAG, "Upgrading from version 0 to version 1.");
      upgrade0to1(context, accountManager, account, product, username, serverURL, profile);
    } else {
      Logger.warn(LOG_TAG, "Don't know how to migrate from version " + currentVersion + " to " + desiredVersion + ".");
    }

    Logger.info(LOG_TAG, "Migrated from version " + currentVersion + " to version " + desiredVersion + ".");
    versionPrefs.edit().putLong(SyncConfiguration.PREF_PREFS_VERSION, desiredVersion).commit();
  }
}
