// Copyright 2016 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.ntp.snippets;

import android.content.Context;

import com.google.android.gms.gcm.GcmNetworkManager;
import com.google.android.gms.gcm.PeriodicTask;
import com.google.android.gms.gcm.Task;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.SuppressFBWarnings;
import org.chromium.chrome.browser.ChromeBackgroundService;
import org.chromium.chrome.browser.externalauth.ExternalAuthUtils;
import org.chromium.chrome.browser.externalauth.UserRecoverableErrorHandler;

/**
 * The {@link SnippetsLauncher} singleton is created and owned by the C++ browser.
 *
 * Thread model: This class is to be run on the UI thread only.
 */
public class SnippetsLauncher {
    private static final String TAG = "SnippetsLauncher";

    // Task tags for fetching snippets.
    public static final String TASK_TAG_WIFI = "FetchSnippetsWifi";
    public static final String TASK_TAG_FALLBACK = "FetchSnippetsFallback";
    // TODO(treib): Remove this after M55.
    private static final String OBSOLETE_TASK_TAG_WIFI_CHARGING = "FetchSnippetsWifiCharging";

    // TODO(treib): Remove this after M55.
    private static final String OBSOLETE_TASK_TAG_RESCHEDULE = "RescheduleSnippets";

    // The amount of "flex" to add around the fetching periods, as a ratio of the period.
    private static final double FLEX_FACTOR = 0.1;

    @VisibleForTesting
    public static final String PREF_IS_SCHEDULED = "ntp_snippets.is_scheduled";

    // The instance of SnippetsLauncher currently owned by a C++ SnippetsLauncherAndroid, if any.
    // If it is non-null then the browser is running.
    private static SnippetsLauncher sInstance;

    private GcmNetworkManager mScheduler;

    private boolean mGCMEnabled = true;

    /**
     * Create a SnippetsLauncher object, which is owned by C++.
     * @param context The app context.
     */
    @VisibleForTesting
    @CalledByNative
    public static SnippetsLauncher create(Context context) {
        if (sInstance != null) {
            throw new IllegalStateException("Already instantiated");
        }

        sInstance = new SnippetsLauncher(context);
        return sInstance;
    }

    /**
     * Called when the C++ counterpart is deleted.
     */
    @VisibleForTesting
    @SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
    @CalledByNative
    public void destroy() {
        assert sInstance == this;
        sInstance = null;
    }

    /**
     * Returns true if the native browser has started and created an instance of {@link
     * SnippetsLauncher}.
     */
    public static boolean hasInstance() {
        return sInstance != null;
    }

    protected SnippetsLauncher(Context context) {
        checkGCM(context);
        mScheduler = GcmNetworkManager.getInstance(context);
    }

    private boolean canUseGooglePlayServices(Context context) {
        return ExternalAuthUtils.getInstance().canUseGooglePlayServices(
                context, new UserRecoverableErrorHandler.Silent());
    }

    private void checkGCM(Context context) {
        // Check to see if Play Services is up to date, and disable GCM if not.
        if (!canUseGooglePlayServices(context)) {
            mGCMEnabled = false;
            Log.i(TAG, "Disabling SnippetsLauncher because Play Services is not up to date.");
        }
    }

    private static PeriodicTask buildFetchTask(
            String tag, long periodSeconds, int requiredNetwork) {
        // Add a bit of "flex" around the target period. This achieves the following:
        // - It makes sure the task doesn't run (significantly) before its initial period has
        //   elapsed. In practice, the scheduler seems to behave like that anyway, but it doesn't
        //   guarantee that, so we shouldn't rely on it.
        // - It gives the scheduler a bit of room to optimize for battery life.
        long effectivePeriodSeconds = (long) (periodSeconds * (1.0 + FLEX_FACTOR));
        long flexSeconds = (long) (periodSeconds * (2.0 * FLEX_FACTOR));
        return new PeriodicTask.Builder()
                .setService(ChromeBackgroundService.class)
                .setTag(tag)
                .setPeriod(effectivePeriodSeconds)
                .setFlex(flexSeconds)
                .setRequiredNetwork(requiredNetwork)
                .setPersisted(true)
                .setUpdateCurrent(true)
                .build();
    }

    private void scheduleOrCancelFetchTask(String taskTag, long period, int requiredNetwork) {
        if (period > 0) {
            mScheduler.schedule(buildFetchTask(taskTag, period, requiredNetwork));
        } else {
            mScheduler.cancelTask(taskTag, ChromeBackgroundService.class);
        }
    }

    @CalledByNative
    private boolean schedule(long periodWifiSeconds, long periodFallbackSeconds) {
        if (!mGCMEnabled) return false;
        Log.i(TAG, "Scheduling: " + periodWifiSeconds + " " + periodFallbackSeconds);

        boolean isScheduled = periodWifiSeconds != 0 || periodFallbackSeconds != 0;
        ContextUtils.getAppSharedPreferences()
                .edit()
                .putBoolean(PREF_IS_SCHEDULED, isScheduled)
                .apply();

        // Google Play Services may not be up to date, if the application was not installed through
        // the Play Store. In this case, scheduling the task will fail silently.
        try {
            mScheduler.cancelTask(OBSOLETE_TASK_TAG_WIFI_CHARGING, ChromeBackgroundService.class);
            scheduleOrCancelFetchTask(
                    TASK_TAG_WIFI, periodWifiSeconds, Task.NETWORK_STATE_UNMETERED);
            scheduleOrCancelFetchTask(
                    TASK_TAG_FALLBACK, periodFallbackSeconds, Task.NETWORK_STATE_CONNECTED);
            mScheduler.cancelTask(OBSOLETE_TASK_TAG_RESCHEDULE, ChromeBackgroundService.class);
        } catch (IllegalArgumentException e) {
            // Disable GCM for the remainder of this session.
            mGCMEnabled = false;

            ContextUtils.getAppSharedPreferences().edit().remove(PREF_IS_SCHEDULED).apply();
            // Return false so that the failure will be logged.
            return false;
        }
        return true;
    }

    @CalledByNative
    private boolean unschedule() {
        if (!mGCMEnabled) return false;
        Log.i(TAG, "Unscheduling");
        return schedule(0, 0);
    }

    public static boolean shouldRescheduleTasksOnUpgrade() {
        // Reschedule the periodic tasks if they were enabled previously.
        return ContextUtils.getAppSharedPreferences().getBoolean(PREF_IS_SCHEDULED, false);
    }
}