/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.settingslib.fuelgauge;

import static android.provider.Settings.Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED;
import static android.provider.Settings.Secure.LOW_POWER_WARNING_ACKNOWLEDGED;

import static com.android.settingslib.fuelgauge.BatterySaverLogging.ACTION_SAVER_STATE_MANUAL_UPDATE;
import static com.android.settingslib.fuelgauge.BatterySaverLogging.EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED;
import static com.android.settingslib.fuelgauge.BatterySaverLogging.EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED_REASON;
import static com.android.settingslib.fuelgauge.BatterySaverLogging.SaverManualEnabledReason;

import android.app.KeyguardManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Slog;

import androidx.core.util.Function;

import com.android.settingslib.flags.Flags;

/**
 * Utilities related to battery saver.
 */
public class BatterySaverUtils {

    private static final String TAG = "BatterySaverUtils";
    /**
     * When set to "true" the notification will be a generic confirm message instead of asking the
     * user if they want to turn on battery saver. If set to false the dialog will specifically
     * talk about battery saver without giving the option of turning it on. The only button visible
     * will be a generic confirmation button to acknowledge the dialog.
     */
    public static final String EXTRA_CONFIRM_TEXT_ONLY = "extra_confirm_only";
    /**
     * Ignored if {@link #EXTRA_CONFIRM_TEXT_ONLY} is "false". Can be set to any of the values in
     * {@link PowerManager.AutoPowerSaveModeTriggers}. If set the dialog will set the power
     * save mode trigger to the specified value after the user acknowledges the trigger.
     */
    public static final String EXTRA_POWER_SAVE_MODE_TRIGGER = "extra_power_save_mode_trigger";
    /**
     * Ignored if {@link #EXTRA_CONFIRM_TEXT_ONLY} is "false". can be set to any value between
     * 0-100 that will be used if {@link #EXTRA_POWER_SAVE_MODE_TRIGGER} is
     * {@link PowerManager#POWER_SAVE_MODE_TRIGGER_PERCENTAGE}.
     */
    public static final String EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL =
            "extra_power_save_mode_trigger_level";

    /** Battery saver schedule keys. */
    public static final String KEY_NO_SCHEDULE = "key_battery_saver_no_schedule";
    public static final String KEY_PERCENTAGE = "key_battery_saver_percentage";

    private BatterySaverUtils() {
    }

    private static final boolean DEBUG = false;

    private static final String SYSUI_PACKAGE = "com.android.systemui";

    /** Broadcast action for SystemUI to show the battery saver confirmation dialog. */
    public static final String ACTION_SHOW_START_SAVER_CONFIRMATION = "PNW.startSaverConfirmation";

    /**
     * Broadcast action for SystemUI to show the notification that suggests turning on
     * automatic battery saver.
     */
    public static final String ACTION_SHOW_AUTO_SAVER_SUGGESTION
            = "PNW.autoSaverSuggestion";

    private static class Parameters {
        private final Context mContext;

        /**
         * We show the auto battery saver suggestion notification when the user manually enables
         * battery saver for the START_NTH time through the END_NTH time.
         * (We won't show it for END_NTH + 1 time and after.)
         */
        private static final int AUTO_SAVER_SUGGESTION_START_NTH = 4;
        private static final int AUTO_SAVER_SUGGESTION_END_NTH = 8;

        public final int startNth;
        public final int endNth;

        public Parameters(Context context) {
            mContext = context;

            final String newValue = Global.getString(mContext.getContentResolver(),
                    Global.LOW_POWER_MODE_SUGGESTION_PARAMS);
            final KeyValueListParser parser = new KeyValueListParser(',');
            try {
                parser.setString(newValue);
            } catch (IllegalArgumentException e) {
                Slog.wtf(TAG, "Bad constants: " + newValue);
            }
            startNth = parser.getInt("start_nth", AUTO_SAVER_SUGGESTION_START_NTH);
            endNth = parser.getInt("end_nth", AUTO_SAVER_SUGGESTION_END_NTH);
        }
    }

    /**
     * Enable / disable battery saver by user request.
     * - If it's the first time and needFirstTimeWarning, show the first time dialog.
     * - If it's 4th time through 8th time, show the schedule suggestion notification.
     *
     * @param enable true to enable battery saver.
     * @return true if the request succeeded.
     */
    public static synchronized boolean setPowerSaveMode(Context context,
            boolean enable, boolean needFirstTimeWarning, @SaverManualEnabledReason int reason) {
        if (DEBUG) {
            Log.d(TAG, "Battery saver turning " + (enable ? "ON" : "OFF") + ", reason: " + reason);
        }
        final ContentResolver cr = context.getContentResolver();
        final PowerManager powerManager = context.getSystemService(PowerManager.class);

        if (Flags.extremePowerLowStateVulnerability()) {
            var keyguardManager = context.getSystemService(KeyguardManager.class);
            if (enable
                    && needFirstTimeWarning
                    && keyguardManager != null
                    && keyguardManager.isDeviceLocked()) {
                var result = powerManager.setPowerSaveModeEnabled(true);
                Log.d(TAG, "Device is locked, setPowerSaveModeEnabled by default. " + result);
                return result;
            }
        }

        final Bundle confirmationExtras = new Bundle(1);
        confirmationExtras.putBoolean(EXTRA_CONFIRM_TEXT_ONLY, false);
        if (enable && needFirstTimeWarning
                && maybeShowBatterySaverConfirmation(context, confirmationExtras)) {
            return false;
        }
        if (enable && !needFirstTimeWarning) {
            setBatterySaverConfirmationAcknowledged(context);
        }

        if (powerManager.setPowerSaveModeEnabled(enable)) {
            if (enable) {
                final int count =
                        Secure.getInt(cr, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, 0) + 1;
                Secure.putInt(cr, Secure.LOW_POWER_MANUAL_ACTIVATION_COUNT, count);

                final Parameters parameters = new Parameters(context);

                if ((count >= parameters.startNth)
                        && (count <= parameters.endNth)
                        && Global.getInt(cr, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0) == 0
                        && Secure.getInt(cr,
                        Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, 0) == 0) {
                    sendSystemUiBroadcast(context, ACTION_SHOW_AUTO_SAVER_SUGGESTION,
                            confirmationExtras);
                }
            }
            recordBatterySaverEnabledReason(context, enable, reason);
            return true;
        }
        return false;
    }

    /**
     * Shows the battery saver confirmation warning if it hasn't been acknowledged by the user in
     * the past before. Various extras can be provided that will change the behavior of this
     * notification as well as the ui for it.
     *
     * @param context A valid context
     * @param extras  Any extras to include in the intent to trigger this confirmation that will
     *                help the system disambiguate what to show/do
     * @return True if it showed the notification because it has not been previously acknowledged.
     * @see #EXTRA_CONFIRM_TEXT_ONLY
     * @see #EXTRA_POWER_SAVE_MODE_TRIGGER
     * @see #EXTRA_POWER_SAVE_MODE_TRIGGER_LEVEL
     */
    public static boolean maybeShowBatterySaverConfirmation(Context context, Bundle extras) {
        if (isBatterySaverConfirmationHasBeenShowedBefore(context)) {
            // Already shown.
            return false;
        }
        sendSystemUiBroadcast(context, ACTION_SHOW_START_SAVER_CONFIRMATION, extras);
        return true;
    }

    /**
     * Returns {@code true} if the battery saver confirmation warning has been acknowledged by the
     * user in the past before.
     */
    public static boolean isBatterySaverConfirmationHasBeenShowedBefore(Context context) {
        Function<String, Integer> secureGetInt =
                key -> Secure.getInt(context.getContentResolver(), key, /* def= */ 0);
        return secureGetInt.apply(LOW_POWER_WARNING_ACKNOWLEDGED) != 0
                && secureGetInt.apply(EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED) != 0;
    }

    private static void recordBatterySaverEnabledReason(Context context, boolean enable,
            @SaverManualEnabledReason int reason) {
        final Bundle enabledReasonExtras = new Bundle(2);
        enabledReasonExtras.putInt(EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED_REASON, reason);
        enabledReasonExtras.putBoolean(EXTRA_POWER_SAVE_MODE_MANUAL_ENABLED, enable);
        sendSystemUiBroadcast(context, ACTION_SAVER_STATE_MANUAL_UPDATE, enabledReasonExtras);
    }

    private static void sendSystemUiBroadcast(Context context, String action, Bundle extras) {
        final Intent intent = new Intent(action);
        intent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
        intent.setPackage(SYSUI_PACKAGE);
        intent.putExtras(extras);
        context.sendBroadcast(intent);
    }

    private static void setBatterySaverConfirmationAcknowledged(Context context) {
        Secure.putInt(context.getContentResolver(),
                Secure.LOW_POWER_WARNING_ACKNOWLEDGED, 1);
        Secure.putInt(context.getContentResolver(),
                Secure.EXTRA_LOW_POWER_WARNING_ACKNOWLEDGED, 1);
    }

    /**
     * Don't show the automatic battery suggestion notification in the future.
     */
    public static void suppressAutoBatterySaver(Context context) {
        Secure.putInt(context.getContentResolver(),
                Secure.SUPPRESS_AUTO_BATTERY_SAVER_SUGGESTION, 1);
    }

    /**
     * Set the automatic battery saver trigger level to {@code level}.
     */
    public static void setAutoBatterySaverTriggerLevel(Context context, int level) {
        if (level > 0) {
            suppressAutoBatterySaver(context);
        }
        Global.putInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, level);
    }

    /**
     * Set the automatic battery saver trigger level to {@code level}, but only when
     * automatic battery saver isn't enabled yet.
     */
    public static void ensureAutoBatterySaver(Context context, int level) {
        if (Global.getInt(context.getContentResolver(), Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0)
                == 0) {
            setAutoBatterySaverTriggerLevel(context, level);
        }
    }

    /**
     * Reverts battery saver schedule mode to none if routine mode is selected.
     *
     * @param context a valid context
     */
    public static void revertScheduleToNoneIfNeeded(Context context) {
        ContentResolver resolver = context.getContentResolver();
        final int currentMode = Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
        if (currentMode == PowerManager.POWER_SAVE_MODE_TRIGGER_DYNAMIC) {
            Global.putInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
            Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                    PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
        }
    }

    /**
     * Gets battery saver schedule mode.
     *
     * @param context a valid context
     * @return battery saver schedule key
     */
    public static String getBatterySaverScheduleKey(Context context) {
        final ContentResolver resolver = context.getContentResolver();
        final int mode = Settings.Global.getInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
        if (mode == PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE) {
            final int threshold =
                    Settings.Global.getInt(resolver, Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
            return threshold <= 0 ? KEY_NO_SCHEDULE : KEY_PERCENTAGE;
        }
        revertScheduleToNoneIfNeeded(context);
        return KEY_NO_SCHEDULE;
    }

    /**
     * Sets battery saver schedule mode.
     *
     * @param context      a valid context
     * @param scheduleKey  {@link #KEY_NO_SCHEDULE} and {@link #KEY_PERCENTAGE}
     * @param triggerLevel for automatic battery saver trigger level
     */
    public static void setBatterySaverScheduleMode(Context context, String scheduleKey,
            int triggerLevel) {
        final ContentResolver resolver = context.getContentResolver();
        switch (scheduleKey) {
            case KEY_NO_SCHEDULE:
                Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                        PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
                Settings.Global.putInt(resolver, Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, 0);
                break;
            case KEY_PERCENTAGE:
                Settings.Global.putInt(resolver, Global.AUTOMATIC_POWER_SAVE_MODE,
                        PowerManager.POWER_SAVE_MODE_TRIGGER_PERCENTAGE);
                Settings.Global.putInt(resolver,
                        Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, triggerLevel);
                break;
            default:
                throw new IllegalStateException("Not a valid schedule key");
        }
    }
}
