// Copyright 2015 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.chrome.browser.sync;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.browser.AppHooks;
import org.chromium.chrome.browser.identity.UniqueIdentificationGenerator;
import org.chromium.chrome.browser.identity.UniqueIdentificationGeneratorFactory;
import org.chromium.chrome.browser.invalidation.InvalidationController;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.signin.AccountManagementFragment;
import org.chromium.chrome.browser.signin.SigninManager;
import org.chromium.chrome.browser.sync.ui.PassphraseActivity;
import org.chromium.components.signin.ChromeSigninController;
import org.chromium.components.sync.AndroidSyncSettings;
import org.chromium.components.sync.ModelType;
import org.chromium.components.sync.PassphraseType;
import org.chromium.components.sync.StopSource;

import javax.annotation.Nullable;

/**
 * SyncController handles the coordination of sync state between the invalidation controller,
 * the Android sync settings, and the native sync code.
 *
 * It also handles initialization of some pieces of sync state on startup.
 *
 * Sync state can be changed from four places:
 *
 * - The Chrome UI, which will call SyncController directly.
 * - Native sync, which can disable it via a dashboard stop and clear.
 * - Android's Chrome sync setting.
 * - Android's master sync setting.
 *
 * SyncController implements listeners for the last three cases. When master sync is disabled, we
 * are careful to not change the Android Chrome sync setting so we know whether to turn sync back
 * on when it is re-enabled.
 */
public class SyncController implements ProfileSyncService.SyncStateChangedListener,
                                       AndroidSyncSettings.AndroidSyncSettingsObserver {
    private static final String TAG = "SyncController";

    /**
     * An identifier for the generator in UniqueIdentificationGeneratorFactory to be used to
     * generate the sync sessions ID. The generator is registered in the Application's onCreate
     * method.
     */
    public static final String GENERATOR_ID = "SYNC";

    @VisibleForTesting
    public static final String SESSION_TAG_PREFIX = "session_sync";

    @SuppressLint("StaticFieldLeak")
    private static SyncController sInstance;
    private static boolean sInitialized;

    private final Context mContext;
    private final ChromeSigninController mChromeSigninController;
    private final ProfileSyncService mProfileSyncService;
    private final SyncNotificationController mSyncNotificationController;

    private SyncController(Context context) {
        mContext = context;
        mChromeSigninController = ChromeSigninController.get();
        AndroidSyncSettings.registerObserver(context, this);
        mProfileSyncService = ProfileSyncService.get();
        mProfileSyncService.addSyncStateChangedListener(this);
        mProfileSyncService.setMasterSyncEnabledProvider(
                new ProfileSyncService.MasterSyncEnabledProvider() {
                    @Override
                    public boolean isMasterSyncEnabled() {
                        return AndroidSyncSettings.isMasterSyncEnabled(mContext);
                    }
                });

        setSessionsId();

        // Create the SyncNotificationController.
        mSyncNotificationController = new SyncNotificationController(
                mContext, PassphraseActivity.class, AccountManagementFragment.class);
        mProfileSyncService.addSyncStateChangedListener(mSyncNotificationController);

        updateSyncStateFromAndroid();

        // When the application gets paused, tell sync to flush the directory to disk.
        ApplicationStatus.registerStateListenerForAllActivities(new ActivityStateListener() {
            @Override
            public void onActivityStateChange(Activity activity, int newState) {
                if (newState == ActivityState.PAUSED) {
                    mProfileSyncService.flushDirectory();
                }
            }
        });

        GmsCoreSyncListener gmsCoreSyncListener = AppHooks.get().createGmsCoreSyncListener();
        if (gmsCoreSyncListener != null) {
            mProfileSyncService.addSyncStateChangedListener(gmsCoreSyncListener);
        }

        SigninManager.get(mContext).addSignInStateObserver(new SigninManager.SignInStateObserver() {
            @Override
            public void onSignedIn() {
                mProfileSyncService.requestStart();
            }

            @Override
            public void onSignedOut() {}
        });
    }

    /**
     * Retrieve the singleton instance of this class.
     *
     * @param context the current context.
     * @return the singleton instance.
     */
    @Nullable
    public static SyncController get(Context context) {
        ThreadUtils.assertOnUiThread();
        if (!sInitialized) {
            if (ProfileSyncService.get() != null) {
                sInstance = new SyncController(context.getApplicationContext());
            }
            sInitialized = true;
        }
        return sInstance;
    }

    /**
     * Updates sync to reflect the state of the Android sync settings.
     */
    private void updateSyncStateFromAndroid() {
        boolean isSyncEnabled = AndroidSyncSettings.isSyncEnabled(mContext);
        if (isSyncEnabled == mProfileSyncService.isSyncRequested()) return;
        if (isSyncEnabled) {
            mProfileSyncService.requestStart();
        } else {
            if (Profile.getLastUsedProfile().isChild()) {
                // For child accounts, Sync needs to stay enabled, so we reenable it in settings.
                // TODO(bauerb): Remove the dependency on child account code and instead go through
                // prefs (here and in the Sync customization UI).
                AndroidSyncSettings.enableChromeSync(mContext);
            } else {
                if (AndroidSyncSettings.isMasterSyncEnabled(mContext)) {
                    RecordHistogram.recordEnumeratedHistogram("Sync.StopSource",
                            StopSource.ANDROID_CHROME_SYNC, StopSource.STOP_SOURCE_LIMIT);
                } else {
                    RecordHistogram.recordEnumeratedHistogram("Sync.StopSource",
                            StopSource.ANDROID_MASTER_SYNC, StopSource.STOP_SOURCE_LIMIT);
                }
                mProfileSyncService.requestStop();
            }
        }
    }

    /**
     * From {@link ProfileSyncService.SyncStateChangedListener}.
     *
     * Changes the invalidation controller and Android sync setting state to match
     * the new native sync state.
     */
    @Override
    public void syncStateChanged() {
        ThreadUtils.assertOnUiThread();
        InvalidationController invalidationController = InvalidationController.get(mContext);
        if (mProfileSyncService.isSyncRequested()) {
            if (!invalidationController.isStarted()) {
                invalidationController.ensureStartedAndUpdateRegisteredTypes();
            }
            if (!AndroidSyncSettings.isSyncEnabled(mContext)) {
                assert AndroidSyncSettings.isMasterSyncEnabled(mContext);
                AndroidSyncSettings.enableChromeSync(mContext);
            }
        } else {
            if (invalidationController.isStarted()) {
                invalidationController.stop();
            }
            if (AndroidSyncSettings.isSyncEnabled(mContext)) {
                // Both Android's master and Chrome sync setting are enabled, so we want to disable
                // the Chrome sync setting to match isSyncRequested. We have to be careful not to
                // disable it when isSyncRequested becomes false due to master sync being disabled
                // so that sync will turn back on if master sync is re-enabled.
                AndroidSyncSettings.disableChromeSync(mContext);
            }
        }
    }

    /**
     * From {@link AndroidSyncSettings.AndroidSyncSettingsObserver}.
     */
    @Override
    public void androidSyncSettingsChanged() {
        ThreadUtils.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                updateSyncStateFromAndroid();
            }
        });
    }

    /**
     * @return Whether sync is enabled to sync urls or open tabs with a non custom passphrase.
     */
    public boolean isSyncingUrlsWithKeystorePassphrase() {
        return mProfileSyncService.isEngineInitialized()
                && mProfileSyncService.getPreferredDataTypes().contains(ModelType.TYPED_URLS)
                && mProfileSyncService.getPassphraseType().equals(
                           PassphraseType.KEYSTORE_PASSPHRASE);
    }

    /**
     * Returns the SyncNotificationController.
     */
    public SyncNotificationController getSyncNotificationController() {
        return mSyncNotificationController;
    }

    /**
     * Set the sessions ID using the generator that was registered for GENERATOR_ID.
     */
    private void setSessionsId() {
        UniqueIdentificationGenerator generator =
                UniqueIdentificationGeneratorFactory.getInstance(GENERATOR_ID);
        String uniqueTag = generator.getUniqueId(null);
        if (uniqueTag.isEmpty()) {
            Log.e(TAG, "Unable to get unique tag for sync. "
                    + "This may lead to unexpected tab sync behavior.");
            return;
        }
        mProfileSyncService.setSessionsId(SESSION_TAG_PREFIX + uniqueTag);
    }
}