// 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.physicalweb;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.components.location.LocationUtils;
import org.json.JSONArray;
import org.json.JSONException;

import java.util.concurrent.TimeUnit;

import javax.annotation.concurrent.ThreadSafe;

/**
 * Centralizes UMA data collection for the Physical Web feature.
 */
@ThreadSafe
public class PhysicalWebUma {
    private static final String TAG = "PhysicalWeb";
    private static final String HAS_DEFERRED_METRICS_KEY = "PhysicalWeb.HasDeferredMetrics";
    private static final String OPT_IN_DECLINE_BUTTON_PRESS_COUNT =
            "PhysicalWeb.OptIn.DeclineButtonPressed";
    private static final String OPT_IN_ENABLE_BUTTON_PRESS_COUNT =
            "PhysicalWeb.OptIn.EnableButtonPressed";
    private static final String OPT_IN_HIGH_PRIORITY_NOTIFICATION_COUNT =
            "PhysicalWeb.OptIn.HighPriorityNotificationShown";
    private static final String OPT_IN_MIN_PRIORITY_NOTIFICATION_COUNT =
            "PhysicalWeb.OptIn.MinPriorityNotificationShown";
    private static final String OPT_IN_NOTIFICATION_PRESS_COUNT =
            "PhysicalWeb.OptIn.NotificationPressed";
    private static final String PREFS_FEATURE_DISABLED_COUNT = "PhysicalWeb.Prefs.FeatureDisabled";
    private static final String PREFS_FEATURE_ENABLED_COUNT = "PhysicalWeb.Prefs.FeatureEnabled";
    private static final String PREFS_LOCATION_DENIED_COUNT = "PhysicalWeb.Prefs.LocationDenied";
    private static final String PREFS_LOCATION_GRANTED_COUNT = "PhysicalWeb.Prefs.LocationGranted";
    private static final String PWS_BACKGROUND_RESOLVE_TIMES = "PhysicalWeb.ResolveTime.Background";
    private static final String PWS_FOREGROUND_RESOLVE_TIMES = "PhysicalWeb.ResolveTime.Foreground";
    private static final String PWS_REFRESH_RESOLVE_TIMES = "PhysicalWeb.ResolveTime.Refresh";
    private static final String OPT_IN_NOTIFICATION_PRESS_DELAYS =
            "PhysicalWeb.ReferralDelay.OptInNotification";
    private static final String STANDARD_NOTIFICATION_PRESS_DELAYS =
            "PhysicalWeb.ReferralDelay.StandardNotification";
    private static final String URL_SELECTED_COUNT = "PhysicalWeb.UrlSelected";
    private static final String TOTAL_URLS_INITIAL_COUNTS =
            "PhysicalWeb.TotalUrls.OnInitialDisplay";
    private static final String TOTAL_URLS_REFRESH_COUNTS =
            "PhysicalWeb.TotalUrls.OnRefresh";
    private static final String ACTIVITY_REFERRALS = "PhysicalWeb.ActivityReferral";
    private static final String PHYSICAL_WEB_STATE = "PhysicalWeb.State";
    private static final String LAUNCH_FROM_PREFERENCES = "LaunchFromPreferences";
    private static final String LAUNCH_FROM_DIAGNOSTICS = "LaunchFromDiagnostics";
    private static final String BLUETOOTH = "Bluetooth";
    private static final String DATA_CONNECTION = "DataConnection";
    private static final String LOCATION_PERMISSION = "LocationPermission";
    private static final String LOCATION_SERVICES = "LocationServices";
    private static final String PREFERENCE = "Preference";
    private static final int BOOLEAN_BOUNDARY = 2;
    private static final int TRISTATE_BOUNDARY = 3;

    /**
     * Records a URL selection.
     */
    public static void onUrlSelected(Context context) {
        handleAction(context, URL_SELECTED_COUNT);
    }

    /**
     * Records a tap on the opt-in decline button.
     */
    public static void onOptInDeclineButtonPressed(Context context) {
        handleAction(context, OPT_IN_DECLINE_BUTTON_PRESS_COUNT);
    }

    /**
     * Records a tap on the opt-in enable button.
     */
    public static void onOptInEnableButtonPressed(Context context) {
        handleAction(context, OPT_IN_ENABLE_BUTTON_PRESS_COUNT);
    }

    /**
     * Records a display of a high priority opt-in notification.
     */
    public static void onOptInHighPriorityNotificationShown(Context context) {
        handleAction(context, OPT_IN_HIGH_PRIORITY_NOTIFICATION_COUNT);
    }

    /**
     * Records a display of a min priority opt-in notification.
     */
    public static void onOptInMinPriorityNotificationShown(Context context) {
        handleAction(context, OPT_IN_MIN_PRIORITY_NOTIFICATION_COUNT);
    }

    /**
     * Records a display of the opt-in activity.
     */
    public static void onOptInNotificationPressed(Context context) {
        handleAction(context, OPT_IN_NOTIFICATION_PRESS_COUNT);
    }

    /**
     * Records when the user disables the Physical Web fetaure.
     */
    public static void onPrefsFeatureDisabled(Context context) {
        handleAction(context, PREFS_FEATURE_DISABLED_COUNT);
    }

    /**
     * Records when the user enables the Physical Web fetaure.
     */
    public static void onPrefsFeatureEnabled(Context context) {
        handleAction(context, PREFS_FEATURE_ENABLED_COUNT);
    }

    /**
     * Records when the user denies the location permission when enabling the Physical Web from the
     * privacy settings menu.
     */
    public static void onPrefsLocationDenied(Context context) {
        handleAction(context, PREFS_LOCATION_DENIED_COUNT);
    }

    /**
     * Records when the user grants the location permission when enabling the Physical Web from the
     * privacy settings menu.
     */
    public static void onPrefsLocationGranted(Context context) {
        handleAction(context, PREFS_LOCATION_GRANTED_COUNT);
    }

    /**
     * Records a response time from PWS for a resolution during a background scan.
     * @param duration The length of time PWS took to respond.
     */
    public static void onBackgroundPwsResolution(Context context, long duration) {
        handleTime(context, PWS_BACKGROUND_RESOLVE_TIMES, duration, TimeUnit.MILLISECONDS);
    }

    /**
     * Records a response time from PWS for a resolution during a foreground scan that is not
     * explicitly user-initiated through a refresh.
     * @param duration The length of time PWS took to respond.
     */
    public static void onForegroundPwsResolution(Context context, long duration) {
        handleTime(context, PWS_FOREGROUND_RESOLVE_TIMES, duration, TimeUnit.MILLISECONDS);
    }

    /**
     * Records a response time from PWS for a resolution during a foreground scan that is explicitly
     * user-initiated through a refresh.
     * @param duration The length of time PWS took to respond.
     */
    public static void onRefreshPwsResolution(Context context, long duration) {
        handleTime(context, PWS_REFRESH_RESOLVE_TIMES, duration, TimeUnit.MILLISECONDS);
    }

    /**
     * Records number of URLs displayed to a user when the URL list is first displayed.
     * @param numUrls The number of URLs displayed to a user.
     */
    public static void onUrlsDisplayed(Context context, int numUrls) {
        if (LibraryLoader.isInitialized()) {
            RecordHistogram.recordCountHistogram(TOTAL_URLS_INITIAL_COUNTS, numUrls);
        } else {
            storeValue(context, TOTAL_URLS_INITIAL_COUNTS, numUrls);
        }
    }

    /**
     * Records number of URLs displayed to a user when the user refreshes the URL list.
     * @param numUrls The number of URLs displayed to a user.
     */
    public static void onUrlsRefreshed(Context context, int numUrls) {
        if (LibraryLoader.isInitialized()) {
            RecordHistogram.recordCountHistogram(TOTAL_URLS_REFRESH_COUNTS, numUrls);
        } else {
            storeValue(context, TOTAL_URLS_REFRESH_COUNTS, numUrls);
        }
    }

    /**
     * Records a ListUrlActivity referral.
     * @param refer The type of referral.  This enum is listed as PhysicalWebActivityReferer in
     *     histograms.xml.
     */
    public static void onActivityReferral(Context context, int referer) {
        handleEnum(context, ACTIVITY_REFERRALS, referer, ListUrlsActivity.REFERER_BOUNDARY);
        switch (referer) {
            case ListUrlsActivity.NOTIFICATION_REFERER:
                handleTime(context, STANDARD_NOTIFICATION_PRESS_DELAYS,
                        UrlManager.getInstance().getTimeSinceNotificationUpdate(),
                        TimeUnit.MILLISECONDS);
                break;
            case ListUrlsActivity.OPTIN_REFERER:
                handleTime(context, OPT_IN_NOTIFICATION_PRESS_DELAYS,
                        UrlManager.getInstance().getTimeSinceNotificationUpdate(),
                        TimeUnit.MILLISECONDS);
                break;
            case ListUrlsActivity.PREFERENCE_REFERER:
                recordPhysicalWebState(context, LAUNCH_FROM_PREFERENCES);
                break;
            case ListUrlsActivity.DIAGNOSTICS_REFERER:
                recordPhysicalWebState(context, LAUNCH_FROM_DIAGNOSTICS);
                break;
            default:
                break;
        }
    }

    /**
     * Calculate a Physical Web state.
     * The Physical Web state includes:
     * - The location provider
     * - The location permission
     * - The bluetooth status
     * - The data connection status
     * - The Physical Web preference status
     */
    public static void recordPhysicalWebState(Context context, String actionName) {
        LocationUtils locationUtils = LocationUtils.getInstance();
        handleEnum(context, createStateString(LOCATION_SERVICES, actionName),
                locationUtils.isSystemLocationSettingEnabled() ? 1 : 0, BOOLEAN_BOUNDARY);
        handleEnum(context, createStateString(LOCATION_PERMISSION, actionName),
                locationUtils.hasAndroidLocationPermission() ? 1 : 0, BOOLEAN_BOUNDARY);
        handleEnum(context, createStateString(BLUETOOTH, actionName),
                Utils.getBluetoothEnabledStatus(), TRISTATE_BOUNDARY);
        handleEnum(context, createStateString(DATA_CONNECTION, actionName),
                Utils.isDataConnectionActive() ? 1 : 0, BOOLEAN_BOUNDARY);
        int preferenceState = 2;
        if (!PhysicalWeb.isOnboarding()) {
            preferenceState = PhysicalWeb.isPhysicalWebPreferenceEnabled() ? 1 : 0;
        }
        handleEnum(context, createStateString(PREFERENCE, actionName),
                preferenceState, TRISTATE_BOUNDARY);
    }

    /**
     * Uploads metrics that we have deferred for uploading.
     */
    public static void uploadDeferredMetrics() {
        // Read the metrics.
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        if (prefs.getBoolean(HAS_DEFERRED_METRICS_KEY, false)) {
            AsyncTask.THREAD_POOL_EXECUTOR.execute(new UmaUploader(prefs));
        }
    }

    private static String createStateString(String stateName, String actionName) {
        return PHYSICAL_WEB_STATE + "." + stateName + "." + actionName;
    }

    private static void storeAction(Context context, String key) {
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        int count = prefs.getInt(key, 0);
        prefs.edit()
                .putBoolean(HAS_DEFERRED_METRICS_KEY, true)
                .putInt(key, count + 1)
                .apply();
    }

    private static void storeValue(Context context, String key, Object value) {
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        SharedPreferences.Editor prefsEditor = prefs.edit();
        JSONArray values = null;
        try {
            values = new JSONArray(prefs.getString(key, "[]"));
            values.put(value);
            prefsEditor
                    .putBoolean(HAS_DEFERRED_METRICS_KEY, true)
                    .putString(key, values.toString())
                    .apply();
        } catch (JSONException e) {
            Log.e(TAG, "JSONException when storing " + key + " stats", e);
            prefsEditor.remove(key).apply();
            return;
        }
        prefsEditor.putString(key, values.toString()).apply();
    }

    private static void handleAction(Context context, String key) {
        if (LibraryLoader.isInitialized()) {
            RecordUserAction.record(key);
        } else {
            storeAction(context, key);
        }
    }

    private static void handleTime(Context context, String key, long duration, TimeUnit tu) {
        if (LibraryLoader.isInitialized()) {
            RecordHistogram.recordTimesHistogram(key, duration, tu);
        } else {
            storeValue(context, key, duration);
        }
    }

    private static void handleEnum(Context context, String key, int value, int boundary) {
        if (LibraryLoader.isInitialized()) {
            RecordHistogram.recordEnumeratedHistogram(key, value, boundary);
        } else {
            storeValue(context, key, value);
        }
    }

    private static class UmaUploader implements Runnable {
        SharedPreferences mPrefs;

        UmaUploader(SharedPreferences prefs) {
            mPrefs = prefs;
        }

        @Override
        public void run() {
            uploadActions(URL_SELECTED_COUNT);
            uploadActions(OPT_IN_DECLINE_BUTTON_PRESS_COUNT);
            uploadActions(OPT_IN_ENABLE_BUTTON_PRESS_COUNT);
            uploadActions(OPT_IN_HIGH_PRIORITY_NOTIFICATION_COUNT);
            uploadActions(OPT_IN_MIN_PRIORITY_NOTIFICATION_COUNT);
            uploadActions(OPT_IN_NOTIFICATION_PRESS_COUNT);
            uploadActions(PREFS_FEATURE_DISABLED_COUNT);
            uploadActions(PREFS_FEATURE_ENABLED_COUNT);
            uploadActions(PREFS_LOCATION_DENIED_COUNT);
            uploadActions(PREFS_LOCATION_GRANTED_COUNT);
            uploadTimes(PWS_BACKGROUND_RESOLVE_TIMES, TimeUnit.MILLISECONDS);
            uploadTimes(PWS_FOREGROUND_RESOLVE_TIMES, TimeUnit.MILLISECONDS);
            uploadTimes(PWS_REFRESH_RESOLVE_TIMES, TimeUnit.MILLISECONDS);
            uploadTimes(STANDARD_NOTIFICATION_PRESS_DELAYS, TimeUnit.MILLISECONDS);
            uploadTimes(OPT_IN_NOTIFICATION_PRESS_DELAYS, TimeUnit.MILLISECONDS);
            uploadCounts(TOTAL_URLS_INITIAL_COUNTS);
            uploadCounts(TOTAL_URLS_REFRESH_COUNTS);
            uploadEnums(ACTIVITY_REFERRALS, ListUrlsActivity.REFERER_BOUNDARY);
            uploadEnums(createStateString(LOCATION_SERVICES, LAUNCH_FROM_DIAGNOSTICS),
                    BOOLEAN_BOUNDARY);
            uploadEnums(createStateString(LOCATION_PERMISSION, LAUNCH_FROM_DIAGNOSTICS),
                    BOOLEAN_BOUNDARY);
            uploadEnums(createStateString(BLUETOOTH, LAUNCH_FROM_DIAGNOSTICS), TRISTATE_BOUNDARY);
            uploadEnums(createStateString(DATA_CONNECTION, LAUNCH_FROM_DIAGNOSTICS),
                    BOOLEAN_BOUNDARY);
            uploadEnums(createStateString(PREFERENCE, LAUNCH_FROM_DIAGNOSTICS), TRISTATE_BOUNDARY);
            uploadEnums(createStateString(LOCATION_SERVICES, LAUNCH_FROM_PREFERENCES),
                    BOOLEAN_BOUNDARY);
            uploadEnums(createStateString(LOCATION_PERMISSION, LAUNCH_FROM_PREFERENCES),
                    BOOLEAN_BOUNDARY);
            uploadEnums(createStateString(BLUETOOTH, LAUNCH_FROM_PREFERENCES), TRISTATE_BOUNDARY);
            uploadEnums(createStateString(DATA_CONNECTION, LAUNCH_FROM_PREFERENCES),
                    BOOLEAN_BOUNDARY);
            uploadEnums(createStateString(PREFERENCE, LAUNCH_FROM_PREFERENCES), TRISTATE_BOUNDARY);
            removePref(HAS_DEFERRED_METRICS_KEY);
        }

        private void removePref(String key) {
            mPrefs.edit()
                    .remove(key)
                    .apply();
        }

        private static Number[] parseJsonNumberArray(String jsonArrayStr) {
            try {
                JSONArray values = new JSONArray(jsonArrayStr);
                Number[] array = new Number[values.length()];
                for (int i = 0; i < values.length(); i++) {
                    Object object = values.get(i);
                    if (!(object instanceof Number)) {
                        return null;
                    }
                    array[i] = (Number) object;
                }
                return array;
            } catch (JSONException e) {
                return null;
            }
        }

        private static Long[] parseJsonLongArray(String jsonArrayStr) {
            Number[] numbers = parseJsonNumberArray(jsonArrayStr);
            if (numbers == null) {
                return null;
            }
            Long[] array = new Long[numbers.length];
            for (int i = 0; i < numbers.length; i++) {
                array[i] = numbers[i].longValue();
            }
            return array;
        }

        private static Integer[] parseJsonIntegerArray(String jsonArrayStr) {
            Number[] numbers = parseJsonNumberArray(jsonArrayStr);
            if (numbers == null) {
                return null;
            }
            Integer[] array = new Integer[numbers.length];
            for (int i = 0; i < numbers.length; i++) {
                array[i] = numbers[i].intValue();
            }
            return array;
        }

        private void uploadActions(String key) {
            int count = mPrefs.getInt(key, 0);
            removePref(key);
            for (int i = 0; i < count; i++) {
                RecordUserAction.record(key);
            }
        }

        private void uploadTimes(final String key, final TimeUnit tu) {
            String jsonTimesStr = mPrefs.getString(key, "[]");
            removePref(key);
            Long[] times = parseJsonLongArray(jsonTimesStr);
            if (times == null) {
                Log.e(TAG, "Error reporting " + key + " with values: " + jsonTimesStr);
                return;
            }
            for (Long time : times) {
                RecordHistogram.recordTimesHistogram(key, time, TimeUnit.MILLISECONDS);
            }
        }

        private void uploadCounts(final String key) {
            String jsonCountsStr = mPrefs.getString(key, "[]");
            removePref(key);
            Integer[] counts = parseJsonIntegerArray(jsonCountsStr);
            if (counts == null) {
                Log.e(TAG, "Error reporting " + key + " with values: " + jsonCountsStr);
                return;
            }
            for (Integer count: counts) {
                RecordHistogram.recordCountHistogram(key, count);
            }
        }

        private void uploadEnums(final String key, int boundary) {
            String jsonEnumsStr = mPrefs.getString(key, "[]");
            removePref(key);
            Integer[] values = parseJsonIntegerArray(jsonEnumsStr);
            if (values == null) {
                Log.e(TAG, "Error reporting " + key + " with values: " + jsonEnumsStr);
                return;
            }
            for (Integer value: values) {
                RecordHistogram.recordEnumeratedHistogram(key, value, boundary);
            }
        }
    }
}