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

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.TargetApi;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.StatFs;
import android.text.TextUtils;
import android.view.View;
import android.view.animation.LinearInterpolator;

import org.chromium.base.CommandLine;
import org.chromium.base.Log;
import org.chromium.base.ThreadUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.appmenu.AppMenu;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
import org.chromium.components.variations.VariationsAssociatedData;
import org.chromium.ui.interpolators.BakedBezierInterpolator;

import java.io.File;

/**
 * Contains logic for whether the update menu item should be shown, whether the update toolbar badge
 * should be shown, and UMA logging for the update menu item.
 */
public class UpdateMenuItemHelper {
    private static final String TAG = "UpdateMenuItemHelper";

    // VariationsAssociatedData configs
    private static final String FIELD_TRIAL_NAME = "UpdateMenuItem";
    private static final String ENABLED_VALUE = "true";
    private static final String ENABLE_UPDATE_MENU_ITEM = "enable_update_menu_item";
    private static final String ENABLE_UPDATE_BADGE = "enable_update_badge";
    private static final String SHOW_SUMMARY = "show_summary";
    private static final String USE_NEW_FEATURES_SUMMARY = "use_new_features_summary";
    private static final String CUSTOM_SUMMARY = "custom_summary";

    // UMA constants for logging whether the menu item was clicked.
    private static final int ITEM_NOT_CLICKED = 0;
    private static final int ITEM_CLICKED_INTENT_LAUNCHED = 1;
    private static final int ITEM_CLICKED_INTENT_FAILED = 2;
    private static final int ITEM_CLICKED_BOUNDARY = 3;

    // UMA constants for logging whether Chrome was updated after the menu item was clicked.
    private static final int UPDATED = 0;
    private static final int NOT_UPDATED = 1;
    private static final int UPDATED_BOUNDARY = 2;

    private static UpdateMenuItemHelper sInstance;
    private static Object sGetInstanceLock = new Object();

    // Whether OmahaClient has already been checked for an update.
    private boolean mAlreadyCheckedForUpdates;

    // Whether an update is available.
    private boolean mUpdateAvailable;

    // URL to direct the user to when Omaha detects a newer version available.
    private String mUpdateUrl;

    // Whether the menu item was clicked. This is used to log the click-through rate.
    private boolean mMenuItemClicked;

    // The latest Chrome version available if OmahaClient.isNewerVersionAvailable() returns true.
    private String mLatestVersion;

    /**
     * @return The {@link UpdateMenuItemHelper} instance.
     */
    public static UpdateMenuItemHelper getInstance() {
        synchronized (UpdateMenuItemHelper.sGetInstanceLock) {
            if (sInstance == null) {
                sInstance = new UpdateMenuItemHelper();
                String testMarketUrl = getStringParamValue(ChromeSwitches.MARKET_URL_FOR_TESTING);
                if (!TextUtils.isEmpty(testMarketUrl)) {
                    sInstance.mUpdateUrl = testMarketUrl;
                }
            }
            return sInstance;
        }
    }

    /**
     * Checks if the {@link OmahaClient} knows about an update.
     * @param activity The current {@link ChromeActivity}.
     */
    public void checkForUpdateOnBackgroundThread(final ChromeActivity activity) {
        if (!getBooleanParam(ENABLE_UPDATE_MENU_ITEM)
                && !getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_ITEM)
                && !getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_BADGE)) {
            return;
        }

        ThreadUtils.assertOnUiThread();

        if (mAlreadyCheckedForUpdates) {
            if (activity.isActivityDestroyed()) return;
            activity.onCheckForUpdate(mUpdateAvailable);
            return;
        }

        mAlreadyCheckedForUpdates = true;

        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                if (OmahaClient.isNewerVersionAvailable(activity)) {
                    mUpdateUrl = OmahaClient.getMarketURL(activity);
                    mLatestVersion = OmahaClient.getLatestVersionNumberString(activity);
                    mUpdateAvailable = true;
                    recordInternalStorageSize();
                } else {
                    mUpdateAvailable = false;
                }
                return null;
            }

            @Override
            protected void onPostExecute(Void result) {
                if (activity.isActivityDestroyed()) return;
                activity.onCheckForUpdate(mUpdateAvailable);
                recordUpdateHistogram();
            }
        }.execute();
    }

    /**
     * Logs whether an update was performed if the update menu item was clicked.
     * Should be called from ChromeActivity#onStart().
     */
    public void onStart() {
        if (mAlreadyCheckedForUpdates) {
            recordUpdateHistogram();
        }
    }

    /**
     * @param activity The current {@link ChromeActivity}.
     * @return Whether the update menu item should be shown.
     */
    public boolean shouldShowMenuItem(ChromeActivity activity) {
        if (getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_ITEM)) {
            return true;
        }

        if (!getBooleanParam(ENABLE_UPDATE_MENU_ITEM)) {
            return false;
        }

        return updateAvailable(activity);
    }

    /**
     * @param context The current {@link Context}.
     * @return The string to use for summary text or the empty string if no summary should be shown.
     */
    public String getMenuItemSummaryText(Context context) {
        if (!getBooleanParam(SHOW_SUMMARY) && !getBooleanParam(USE_NEW_FEATURES_SUMMARY)
                && !getBooleanParam(CUSTOM_SUMMARY)) {
            return "";
        }

        String customSummary = getStringParamValue(CUSTOM_SUMMARY);
        if (!TextUtils.isEmpty(customSummary)) {
            return customSummary;
        }

        if (getBooleanParam(USE_NEW_FEATURES_SUMMARY)) {
            return context.getResources().getString(R.string.menu_update_summary_new_features);
        }

        return context.getResources().getString(R.string.menu_update_summary_default);
    }

    /**
     * @param activity The current {@link ChromeActivity}.
     * @return Whether the update badge should be shown in the toolbar.
     */
    public boolean shouldShowToolbarBadge(ChromeActivity activity) {
        if (getBooleanParam(ChromeSwitches.FORCE_SHOW_UPDATE_MENU_BADGE)) {
            return true;
        }

        // The badge is hidden if the update menu item has been clicked until there is an
        // even newer version of Chrome available.
        String latestVersionWhenClicked =
                PrefServiceBridge.getInstance().getLatestVersionWhenClickedUpdateMenuItem();
        if (!getBooleanParam(ENABLE_UPDATE_BADGE)
                || TextUtils.equals(latestVersionWhenClicked, mLatestVersion)) {
            return false;
        }

        return updateAvailable(activity);
    }

    /**
     * Handles a click on the update menu item.
     * @param activity The current {@link ChromeActivity}.
     */
    public void onMenuItemClicked(ChromeActivity activity) {
        if (mUpdateUrl == null) return;

        // If the update menu item is showing because it was forced on through about://flags
        // then mLatestVersion may be null.
        if (mLatestVersion != null) {
            PrefServiceBridge.getInstance().setLatestVersionWhenClickedUpdateMenuItem(
                    mLatestVersion);
        }

        // Fire an intent to open the URL.
        try {
            Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUpdateUrl));
            activity.startActivity(launchIntent);
            recordItemClickedHistogram(ITEM_CLICKED_INTENT_LAUNCHED);
            PrefServiceBridge.getInstance().setClickedUpdateMenuItem(true);
        } catch (ActivityNotFoundException e) {
            Log.e(TAG, "Failed to launch Activity for: %s", mUpdateUrl);
            recordItemClickedHistogram(ITEM_CLICKED_INTENT_FAILED);
        }
    }

    /**
     * Should be called before the AppMenu is dismissed if the update menu item was clicked.
     */
    public void setMenuItemClicked() {
        mMenuItemClicked = true;
    }

    /**
     * Called when the {@link AppMenu} is dimissed. Logs a histogram immediately if the update menu
     * item was not clicked. If it was clicked, logging is delayed until #onMenuItemClicked().
     */
    public void onMenuDismissed() {
        if (!mMenuItemClicked) {
            recordItemClickedHistogram(ITEM_NOT_CLICKED);
        }
        mMenuItemClicked = false;
    }

    /**
     * Creates an {@link AnimatorSet} for showing the update badge that is displayed on top
     * of the app menu button.
     *
     * @param menuButton The {@link View} containing the app menu button.
     * @param menuBadge The {@link View} containing the update badge.
     * @return An {@link AnimatorSet} to run when showing the update badge.
     */
    public static AnimatorSet createShowUpdateBadgeAnimation(final View menuButton,
            final View menuBadge) {
        // Create badge ObjectAnimators.
        ObjectAnimator badgeFadeAnimator = ObjectAnimator.ofFloat(menuBadge, View.ALPHA, 1.f);
        badgeFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);

        int pixelTranslation = menuBadge.getResources().getDimensionPixelSize(
                R.dimen.menu_badge_translation_y_distance);
        ObjectAnimator badgeTranslateYAnimator = ObjectAnimator.ofFloat(menuBadge,
                View.TRANSLATION_Y, pixelTranslation, 0.f);
        badgeTranslateYAnimator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE);

        // Create menu button ObjectAnimator.
        ObjectAnimator menuButtonFadeAnimator = ObjectAnimator.ofFloat(menuButton, View.ALPHA, 0.f);
        menuButtonFadeAnimator.setInterpolator(new LinearInterpolator());

        // Create AnimatorSet and listeners.
        AnimatorSet set = new AnimatorSet();
        set.playTogether(badgeFadeAnimator, badgeTranslateYAnimator, menuButtonFadeAnimator);
        set.setDuration(350);
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // Make sure the menu button is visible again.
                menuButton.setAlpha(1.f);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                // Jump to the end state if the animation is canceled.
                menuBadge.setAlpha(1.f);
                menuBadge.setTranslationY(0.f);
                menuButton.setAlpha(1.f);
            }
        });

        return set;
    }

    /**
     * Creates an {@link AnimatorSet} for hiding the update badge that is displayed on top
     * of the app menu button.
     *
     * @param menuButton The {@link View} containing the app menu button.
     * @param menuBadge The {@link View} containing the update badge.
     * @return An {@link AnimatorSet} to run when hiding the update badge.
     */
    public static AnimatorSet createHideUpdateBadgeAnimation(final View menuButton,
            final View menuBadge) {
        // Create badge ObjectAnimator.
        ObjectAnimator badgeFadeAnimator = ObjectAnimator.ofFloat(menuBadge, View.ALPHA, 0.f);
        badgeFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_OUT_CURVE);

        // Create menu button ObjectAnimator.
        ObjectAnimator menuButtonFadeAnimator = ObjectAnimator.ofFloat(menuButton, View.ALPHA, 1.f);
        menuButtonFadeAnimator.setInterpolator(BakedBezierInterpolator.FADE_IN_CURVE);

        // Create AnimatorSet and listeners.
        AnimatorSet set = new AnimatorSet();
        set.playTogether(badgeFadeAnimator, menuButtonFadeAnimator);
        set.setDuration(200);
        set.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                menuBadge.setVisibility(View.GONE);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                // Jump to the end state if the animation is canceled.
                menuButton.setAlpha(1.f);
                menuBadge.setVisibility(View.GONE);
            }
        });

        return set;
    }

    private boolean updateAvailable(ChromeActivity activity) {
        if (!mAlreadyCheckedForUpdates) {
            checkForUpdateOnBackgroundThread(activity);
            return false;
        }

        return mUpdateAvailable;
    }

    private void recordItemClickedHistogram(int action) {
        RecordHistogram.recordEnumeratedHistogram("GoogleUpdate.MenuItem.ActionTakenOnMenuOpen",
                action, ITEM_CLICKED_BOUNDARY);
    }

    private void recordUpdateHistogram() {
        if (PrefServiceBridge.getInstance().getClickedUpdateMenuItem()) {
            RecordHistogram.recordEnumeratedHistogram(
                    "GoogleUpdate.MenuItem.ActionTakenAfterItemClicked",
                    mUpdateAvailable ? NOT_UPDATED : UPDATED, UPDATED_BOUNDARY);
            PrefServiceBridge.getInstance().setClickedUpdateMenuItem(false);
        }
    }

    /**
     * Gets a boolean VariationsAssociatedData parameter, assuming the <paramName>="true" format.
     * Also checks for a command-line switch with the same name, for easy local testing.
     * @param paramName The name of the parameter (or command-line switch) to get a value for.
     * @return Whether the param is defined with a value "true", if there's a command-line
     *         flag present with any value.
     */
    private static boolean getBooleanParam(String paramName) {
        if (CommandLine.getInstance().hasSwitch(paramName)) {
            return true;
        }
        return TextUtils.equals(ENABLED_VALUE,
                VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName));
    }

    /**
     * Gets a String VariationsAssociatedData parameter. Also checks for a command-line switch with
     * the same name, for easy local testing.
     * @param paramName The name of the parameter (or command-line switch) to get a value for.
     * @return The command-line flag value if present, or the param is value if present.
     */
    private static String getStringParamValue(String paramName) {
        String value = CommandLine.getInstance().getSwitchValue(paramName);
        if (TextUtils.isEmpty(value)) {
            value = VariationsAssociatedData.getVariationParamValue(FIELD_TRIAL_NAME, paramName);
        }
        return value;
    }

    private void recordInternalStorageSize() {
        assert !ThreadUtils.runningOnUiThread();

        File path = Environment.getDataDirectory();
        StatFs statFs = new StatFs(path.getAbsolutePath());
        long size;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            size = getSize(statFs);
        } else {
            size = getSizeUpdatedApi(statFs);
        }
        RecordHistogram.recordLinearCountHistogram(
                "GoogleUpdate.InfoBar.InternalStorageSizeAvailable", (int) size, 1, 200, 100);
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private static long getSizeUpdatedApi(StatFs statFs) {
        return statFs.getAvailableBytes() / (1024 * 1024);
    }

    @SuppressWarnings("deprecation")
    private static long getSize(StatFs statFs) {
        int blockSize = statFs.getBlockSize();
        int availableBlocks = statFs.getAvailableBlocks();
        return (blockSize * availableBlocks) / (1024 * 1024);
    }
}