// Copyright 2013 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.accounts.Account;
import android.app.Application;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;

import com.google.common.annotations.VisibleForTesting;
import com.google.protos.ipc.invalidation.Types;

import org.chromium.base.ThreadUtils;
import org.chromium.content.browser.BrowserStartupController;

import java.util.concurrent.Semaphore;

/**
 * A sync adapter for Chromium.
 */
public abstract class ChromiumSyncAdapter extends AbstractThreadedSyncAdapter {
    private static final String TAG = "ChromiumSyncAdapter";

    // TODO(nyquist) Make these fields package protected once downstream sync adapter tests are
    // removed.
    @VisibleForTesting
    public static final String INVALIDATION_OBJECT_SOURCE_KEY = "objectSource";
    @VisibleForTesting
    public static final String INVALIDATION_OBJECT_ID_KEY = "objectId";
    @VisibleForTesting
    public static final String INVALIDATION_VERSION_KEY = "version";
    @VisibleForTesting
    public static final String INVALIDATION_PAYLOAD_KEY = "payload";

    private final Application mApplication;
    private final boolean mAsyncStartup;

    public ChromiumSyncAdapter(Context context, Application application) {
        super(context, false);
        mApplication = application;
        mAsyncStartup = useAsyncStartup();
    }

    protected abstract boolean useAsyncStartup();

    protected abstract void initCommandLine();

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority,
                              ContentProviderClient provider, SyncResult syncResult) {
        if (!DelayedSyncController.getInstance().shouldPerformSync(getContext(), extras, account)) {
            return;
        }

        // Browser startup is asynchronous, so we will need to wait for startup to finish.
        Semaphore semaphore = new Semaphore(0);

        // Configure the callback with all the data it needs.
        BrowserStartupController.StartupCallback callback =
                getStartupCallback(mApplication, account, extras, syncResult, semaphore);
        startBrowserProcess(callback, syncResult, semaphore);

        try {
            // Wait for startup to complete.
            semaphore.acquire();
        } catch (InterruptedException e) {
            Log.w(TAG, "Got InterruptedException when trying to request a sync.", e);
            // Using numIoExceptions so Android will treat this as a soft error.
            syncResult.stats.numIoExceptions++;
        }
    }

    private void startBrowserProcess(
            final BrowserStartupController.StartupCallback callback,
            final SyncResult syncResult, Semaphore semaphore) {
        try {
            ThreadUtils.runOnUiThreadBlocking(new Runnable() {
                @Override
                public void run() {
                    initCommandLine();
                    if (mAsyncStartup) {
                        BrowserStartupController.get(mApplication)
                                .startBrowserProcessesAsync(callback);
                    } else {
                        startBrowserProcessesSync(callback);
                    }
                }
            });
        } catch (RuntimeException e) {
            // It is still unknown why we ever experience this. See http://crbug.com/180044.
            Log.w(TAG, "Got exception when trying to request a sync. Informing Android system.", e);
            // Using numIoExceptions so Android will treat this as a soft error.
            syncResult.stats.numIoExceptions++;
            semaphore.release();
        }
    }

    private void startBrowserProcessesSync(
            final BrowserStartupController.StartupCallback callback) {
        if (BrowserStartupController.get(mApplication).startBrowserProcessesSync(
                BrowserStartupController.MAX_RENDERERS_LIMIT)) {
            new Handler().post(new Runnable() {
                @Override
                public void run() {
                    callback.onSuccess(false);
                }
            });
        } else {
            Log.e(TAG, "Unable to start browser process.");
            new Handler().post(new Runnable() {
                @Override
                public void run() {
                    callback.onFailure();
                }
            });
        }
    }

    private BrowserStartupController.StartupCallback getStartupCallback(
            final Context context, final Account acct, Bundle extras,
            final SyncResult syncResult, final Semaphore semaphore) {
        final boolean syncAllTypes = extras.getString(INVALIDATION_OBJECT_ID_KEY) == null;
        final int objectSource = syncAllTypes ? 0 : extras.getInt(INVALIDATION_OBJECT_SOURCE_KEY);
        final String objectId = syncAllTypes ? "" : extras.getString(INVALIDATION_OBJECT_ID_KEY);
        final long version = syncAllTypes ? 0 : extras.getLong(INVALIDATION_VERSION_KEY);
        final String payload = syncAllTypes ? "" : extras.getString(INVALIDATION_PAYLOAD_KEY);

        return new BrowserStartupController.StartupCallback() {
            @Override
            public void onSuccess(boolean alreadyStarted) {
                // Startup succeeded, so we can tickle the sync engine.
                if (syncAllTypes) {
                    Log.v(TAG, "Received sync tickle for all types.");
                    requestSyncForAllTypes();
                } else {
                    // Invalidations persisted before objectSource was added should be assumed to be
                    // for Sync objects. TODO(stepco): Remove this check once all persisted
                    // invalidations can be expected to have the objectSource.
                    int resolvedSource = objectSource;
                    if (resolvedSource == 0) {
                      resolvedSource = Types.ObjectSource.Type.CHROME_SYNC.getNumber();
                    }
                    Log.v(TAG, "Received sync tickle for " + resolvedSource + " " + objectId + ".");
                    requestSync(resolvedSource, objectId, version, payload);
                }
                semaphore.release();
            }

            @Override
            public void onFailure() {
                // The startup failed, so we reset the delayed sync state.
                DelayedSyncController.getInstance().setDelayedSync(context, acct.name);
                // Using numIoExceptions so Android will treat this as a soft error.
                syncResult.stats.numIoExceptions++;
                semaphore.release();
            }
        };
    }

    @VisibleForTesting
    public void requestSync(int objectSource, String objectId, long version, String payload) {
        ProfileSyncService.get(mApplication)
                .requestSyncFromNativeChrome(objectSource, objectId, version, payload);
    }

    @VisibleForTesting
    public void requestSyncForAllTypes() {
        ProfileSyncService.get(mApplication).requestSyncFromNativeChromeForAllTypes();
    }
}