/**
 * Copyright 2013 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.drive.appdatapreferences;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;

import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;

import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;
import com.google.drive.appdatapreferences.tasks.GetOrCreatePreferencesDriveTask;
import com.google.drive.appdatapreferences.tasks.UpdatePreferencesDriveTask;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;

/**
 * Manages the synchronization between an appdata preferences file
 * and a local SharedPreferences instance.
 *
 * @author jbd@google.com (Burcu Dogan)
 */
public class AppdataPreferencesSyncer {

  private static AppdataPreferencesSyncer sInstance;

  private final Context mContext;
  private GoogleAccountCredential mCredential;
  private SharedPreferences mPreferences;
  private OnChangeListener mOnChangeListener;
  private OnUserRecoverableAuthExceptionExceptionListener mOnExceptionListener;
  private AppdataPreferencesSyncManager mSyncManager;
  private String mLastSyncedJson;

  /**
   * Gets the singleton {@code AppdataPreferencesSyncer} instance.
   * @param context Context of the application
   */
  public static AppdataPreferencesSyncer get(Context context) {
    if (sInstance == null) {
      sInstance = new AppdataPreferencesSyncer(context);
    }
    return sInstance;
  }

  /**
   * Private {@code AppdataPreferencesSyncer} constructor.
   * @param context Context of the application
   */
  private AppdataPreferencesSyncer(Context context) {
    mContext = context;
  }
  
  /**
   * Binds a {@code SharedPreferences} object to a Google account.
   * @param credential  Google account credentials.
   * @param preferences Preferences to be bound to the Google account.
   */
  public void bind(GoogleAccountCredential credential,
      SharedPreferences preferences) {
    setCredential(credential);
    setPreferences(preferences);
  }

  /**
   * Syncs the preferences file with an appdata preferences file.
   *
   * Synchronization steps:
   * 1. If there are local changes, sync the latest local version with remote
   *    and ignore merge conflicts. The last write wins.
   * 2. If there are no local changes, fetch the latest remote version. If
   *    it includes changes, notify that preferences have changed.
   */
  public synchronized void sync() {
    // TODO: don't silently ignore the sync operation
    // notify user that preferences and credential are not set.
    if (mPreferences == null || mCredential == null) {
      return;
    }
    // check if the values are changed since last update
    Map<String, ?> values = mPreferences.getAll();
    String localJson = GSON.toJson(values);
    try {
      if (localJson != null && !localJson.equals(mLastSyncedJson)) {
        updateRemote(localJson);
      } else {
        updateLocal();
      }
    } catch (IOException e) {
      handleException(e);
    }
  }

  /**
   * Gets the context.
   */
  public Context getContext() {
    return mContext;
  }

  /**
   * Gets the sync manager.
   */
  public AppdataPreferencesSyncManager getSyncManager() {
    return mSyncManager;
  }

  /**
   * Gets the credential.
   */
  public GoogleAccountCredential getCredential() {
    return mCredential;
  }

  /**
   * Gets the preferences.
   */
  public SharedPreferences getPreferences() {
    return mPreferences;
  }

  /**
   * Constructs a Drive service in the current context and with the
   * credentials use to initiate AppdataPreferences instance.
   * @return Drive service instance.
   */
  public Drive getDriveService() {
    Drive service = new Drive.Builder(
        AndroidHttp.newCompatibleTransport(),
        new GsonFactory(),
        mCredential).build();
    return service;
  }

  /**
   * Sets the credential and starts a periodic sync for the
   * selected account.
   * @param credential User's credential
   */
  public void setCredential(GoogleAccountCredential credential) {
    mCredential = credential;
    mSyncManager =
        new AppdataPreferencesSyncManager(credential.getSelectedAccount());
    mSyncManager.startPeriodicSync();
  }

  /**
   * Sets the {@code SharedPreferences} instance.
   * @param preferences {@code SharedPreferences} instance to be synced.
   */
  public void setPreferences(SharedPreferences preferences) {
    mPreferences = preferences;
  }

  /**
   * Sets a {@code UserRecoverableAuthExceptionListener}.
   * @param listener
   */
  public void setOnUserRecoverableAuthExceptionListener(
      OnUserRecoverableAuthExceptionExceptionListener listener) {
    mOnExceptionListener = listener;
  }

  /**
   * Sets the OnChangeListener.
   * @param listener
   */
  public void setOnChangeListener(OnChangeListener listener) {
    mOnChangeListener = listener;
  }

  /**
   * Updates the remote preferences file with the given JSON content.
   * @param json    New contents of the remote preferences file in JSON.
   * @throws IOException
   */
  private void updateRemote(String json) throws IOException {
    Log.d(TAG, "Updating the remote preferences file");
    // update the remote
    new UpdatePreferencesDriveTask(getDriveService()).execute(json);
    mLastSyncedJson = json;
  }

  /**
   * Updates the local SharedPreferences instance with the remote
   * changes and calls OnChangeListener.
   * @throws IOException
   */
  private void updateLocal() throws IOException {
    Log.d(TAG, "Updating the local preferences file");
    // update the local preferences
    HashMap<String, Object> remoteObj = null;
    String json =
        new GetOrCreatePreferencesDriveTask(getDriveService()).execute();
    Type type = new TypeToken<HashMap<String, Object>>() {}.getType();
    remoteObj = GSON.fromJson(json, type);
    Utils.replaceValues(mPreferences, remoteObj);
    // Notify if there are changes
    if (json != mLastSyncedJson && mOnChangeListener != null) {
      mOnChangeListener.onChange(mPreferences);
      mLastSyncedJson = json;
    }
  }

  /**
   * Handles API exceptions and notifies OnExceptionListener
   * if given exception is a UserRecoverableAuthIOException.
   * @param exception Exception to handle
   */
  private void handleException(Exception exception) {
    if (mOnExceptionListener == null) {
      return;
    }
    if (exception instanceof UserRecoverableAuthIOException) {
      mOnExceptionListener.onUserRecoverableAuthException(
          (UserRecoverableAuthIOException)exception);
    } else {
      exception.printStackTrace();
    }
  }

  /**
   * Called when remote preferences file is changed and changes
   * are synced to the local SharedPreferences instance.
   */
  public interface OnChangeListener {
    public void onChange(SharedPreferences prefs);
  }

  /**
   * Called when Google Drive API requests responds with a 401 or 403 and
   * user's permission is required.
   */
  public interface OnUserRecoverableAuthExceptionExceptionListener {
    public void onUserRecoverableAuthException(
        UserRecoverableAuthIOException ex);
  }

  final public static Gson GSON = new GsonBuilder().create();
  final public static String TAG = "syncer";

}
