/*
 * Copyright (C) 2016 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.settings.fuelgauge;

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.AsyncTask;
import android.os.BatteryManager;
import android.os.BatteryStats.HistoryItem;
import android.os.BatteryStatsManager;
import android.os.BatteryUsageStats;
import android.os.SystemClock;
import android.provider.Settings;
import android.text.format.Formatter;
import android.util.Log;
import android.util.SparseIntArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;

import com.android.internal.os.BatteryStatsHistoryIterator;
import com.android.settings.Utils;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.UsageView;
import com.android.settingslib.R;
import com.android.settingslib.fuelgauge.BatteryStatus;
import com.android.settingslib.fuelgauge.Estimate;
import com.android.settingslib.fuelgauge.EstimateKt;
import com.android.settingslib.utils.PowerUtil;
import com.android.settingslib.utils.StringUtil;

public class BatteryInfo {
    private static final String TAG = "BatteryInfo";

    public CharSequence chargeLabel;
    public CharSequence remainingLabel;
    public int batteryLevel;
    public int batteryStatus;
    public int pluggedStatus;
    public boolean discharging = true;
    public boolean isBatteryDefender;
    public boolean isFastCharging;
    public long remainingTimeUs = 0;
    public long averageTimeToDischarge = EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN;
    public String batteryPercentString;
    public String statusLabel;
    public String suggestionLabel;
    private boolean mCharging;
    private BatteryUsageStats mBatteryUsageStats;
    private static final String LOG_TAG = "BatteryInfo";
    private long timePeriod;

    public interface Callback {
        void onBatteryInfoLoaded(BatteryInfo info);
    }

    public void bindHistory(final UsageView view, BatteryDataParser... parsers) {
        final Context context = view.getContext();
        BatteryDataParser parser =
                new BatteryDataParser() {
                    SparseIntArray mPoints = new SparseIntArray();
                    long mStartTime;
                    int mLastTime = -1;
                    byte mLastLevel;

                    @Override
                    public void onParsingStarted(long startTime, long endTime) {
                        this.mStartTime = startTime;
                        timePeriod = endTime - startTime;
                        view.clearPaths();
                        // Initially configure the graph for history only.
                        view.configureGraph((int) timePeriod, 100);
                    }

                    @Override
                    public void onDataPoint(long time, HistoryItem record) {
                        mLastTime = (int) time;
                        mLastLevel = record.batteryLevel;
                        mPoints.put(mLastTime, mLastLevel);
                    }

                    @Override
                    public void onDataGap() {
                        if (mPoints.size() > 1) {
                            view.addPath(mPoints);
                        }
                        mPoints.clear();
                    }

                    @Override
                    public void onParsingDone() {
                        onDataGap();

                        // Add projection if we have an estimate.
                        if (remainingTimeUs != 0) {
                            PowerUsageFeatureProvider provider =
                                    FeatureFactory.getFeatureFactory()
                                            .getPowerUsageFeatureProvider();
                            if (!mCharging
                                    && provider.isEnhancedBatteryPredictionEnabled(context)) {
                                mPoints =
                                        provider.getEnhancedBatteryPredictionCurve(
                                                context, mStartTime);
                            } else {
                                // Linear extrapolation.
                                if (mLastTime >= 0) {
                                    mPoints.put(mLastTime, mLastLevel);
                                    mPoints.put(
                                            (int)
                                                    (timePeriod
                                                            + PowerUtil.convertUsToMs(
                                                                    remainingTimeUs)),
                                            mCharging ? 100 : 0);
                                }
                            }
                        }

                        // If we have a projection, reconfigure the graph to show it.
                        if (mPoints != null && mPoints.size() > 0) {
                            int maxTime = mPoints.keyAt(mPoints.size() - 1);
                            view.configureGraph(maxTime, 100);
                            view.addProjectedPath(mPoints);
                        }
                    }
                };
        BatteryDataParser[] parserList = new BatteryDataParser[parsers.length + 1];
        for (int i = 0; i < parsers.length; i++) {
            parserList[i] = parsers[i];
        }
        parserList[parsers.length] = parser;
        parseBatteryHistory(parserList);
        String timeString =
                context.getString(
                        R.string.charge_length_format,
                        Formatter.formatShortElapsedTime(context, timePeriod));
        String remaining = "";
        if (remainingTimeUs != 0) {
            remaining =
                    context.getString(
                            R.string.remaining_length_format,
                            Formatter.formatShortElapsedTime(context, remainingTimeUs / 1000));
        }
        view.setBottomLabels(new CharSequence[] {timeString, remaining});
    }

    /** Gets battery info */
    public static void getBatteryInfo(
            final Context context, final Callback callback, boolean shortString) {
        BatteryInfo.getBatteryInfo(context, callback, /* batteryUsageStats */ null, shortString);
    }

    static long getSettingsChargeTimeRemaining(final Context context) {
        return Settings.Global.getLong(
                context.getContentResolver(),
                com.android.settingslib.fuelgauge.BatteryUtils.GLOBAL_TIME_TO_FULL_MILLIS,
                -1);
    }

    /** Gets battery info */
    public static void getBatteryInfo(
            final Context context,
            final Callback callback,
            @Nullable final BatteryUsageStats batteryUsageStats,
            boolean shortString) {
        new AsyncTask<Void, Void, BatteryInfo>() {
            @Override
            protected BatteryInfo doInBackground(Void... params) {
                boolean shouldCloseBatteryUsageStats = false;
                BatteryUsageStats stats;
                if (batteryUsageStats != null) {
                    stats = batteryUsageStats;
                } else {
                    try {
                        stats =
                                context.getSystemService(BatteryStatsManager.class)
                                        .getBatteryUsageStats();
                        shouldCloseBatteryUsageStats = true;
                    } catch (RuntimeException e) {
                        Log.e(TAG, "getBatteryInfo() from getBatteryUsageStats()", e);
                        // Use default BatteryUsageStats.
                        stats = new BatteryUsageStats.Builder(new String[0]).build();
                    }
                }
                final BatteryInfo batteryInfo = getBatteryInfo(context, stats, shortString);
                if (shouldCloseBatteryUsageStats) {
                    try {
                        stats.close();
                    } catch (Exception e) {
                        Log.e(TAG, "BatteryUsageStats.close() failed", e);
                    }
                }
                return batteryInfo;
            }

            @Override
            protected void onPostExecute(BatteryInfo batteryInfo) {
                final long startTime = System.currentTimeMillis();
                callback.onBatteryInfoLoaded(batteryInfo);
                BatteryUtils.logRuntime(LOG_TAG, "time for callback", startTime);
            }
        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    }

    /** Creates a BatteryInfo based on BatteryUsageStats */
    @WorkerThread
    public static BatteryInfo getBatteryInfo(
            final Context context,
            @NonNull final BatteryUsageStats batteryUsageStats,
            boolean shortString) {
        final long batteryStatsTime = System.currentTimeMillis();
        BatteryUtils.logRuntime(LOG_TAG, "time for getStats", batteryStatsTime);

        final long startTime = System.currentTimeMillis();
        PowerUsageFeatureProvider provider =
                FeatureFactory.getFeatureFactory().getPowerUsageFeatureProvider();
        final long elapsedRealtimeUs = PowerUtil.convertMsToUs(SystemClock.elapsedRealtime());

        final Intent batteryBroadcast =
                context.registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        // 0 means we are discharging, anything else means charging
        final boolean discharging =
                batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) == 0;

        if (discharging && provider.isEnhancedBatteryPredictionEnabled(context)) {
            Estimate estimate = provider.getEnhancedBatteryPrediction(context);
            if (estimate != null) {
                Estimate.storeCachedEstimate(context, estimate);
                BatteryUtils.logRuntime(LOG_TAG, "time for enhanced BatteryInfo", startTime);
                return BatteryInfo.getBatteryInfo(
                        context,
                        batteryBroadcast,
                        batteryUsageStats,
                        estimate,
                        elapsedRealtimeUs,
                        shortString);
            }
        }
        final long prediction = discharging ? batteryUsageStats.getBatteryTimeRemainingMs() : 0;
        final Estimate estimate =
                new Estimate(
                        prediction,
                        false, /* isBasedOnUsage */
                        EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
        BatteryUtils.logRuntime(LOG_TAG, "time for regular BatteryInfo", startTime);
        return BatteryInfo.getBatteryInfo(
                context,
                batteryBroadcast,
                batteryUsageStats,
                estimate,
                elapsedRealtimeUs,
                shortString);
    }

    @WorkerThread
    public static BatteryInfo getBatteryInfoOld(
            Context context,
            Intent batteryBroadcast,
            BatteryUsageStats batteryUsageStats,
            long elapsedRealtimeUs,
            boolean shortString) {
        Estimate estimate =
                new Estimate(
                        batteryUsageStats.getBatteryTimeRemainingMs(),
                        false,
                        EstimateKt.AVERAGE_TIME_TO_DISCHARGE_UNKNOWN);
        return getBatteryInfo(
                context,
                batteryBroadcast,
                batteryUsageStats,
                estimate,
                elapsedRealtimeUs,
                shortString);
    }

    @WorkerThread
    public static BatteryInfo getBatteryInfo(
            Context context,
            Intent batteryBroadcast,
            @NonNull BatteryUsageStats batteryUsageStats,
            Estimate estimate,
            long elapsedRealtimeUs,
            boolean shortString,
            long currentTimeMs) {
        final boolean isCompactStatus =
                context.getResources()
                        .getBoolean(com.android.settings.R.bool.config_use_compact_battery_status);
        BatteryInfo info = new BatteryInfo();
        info.mBatteryUsageStats = batteryUsageStats;
        info.batteryLevel = Utils.getBatteryLevel(batteryBroadcast);
        info.batteryPercentString = Utils.formatPercentage(info.batteryLevel);
        info.pluggedStatus = batteryBroadcast.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0);
        info.mCharging = info.pluggedStatus != 0;
        info.averageTimeToDischarge = estimate.getAverageDischargeTime();
        info.isBatteryDefender =
                batteryBroadcast.getIntExtra(
                                BatteryManager.EXTRA_CHARGING_STATUS,
                                BatteryManager.CHARGING_POLICY_DEFAULT)
                        == BatteryManager.CHARGING_POLICY_ADAPTIVE_LONGLIFE;

        info.statusLabel = Utils.getBatteryStatus(context, batteryBroadcast, isCompactStatus);
        info.batteryStatus =
                batteryBroadcast.getIntExtra(
                        BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
        info.isFastCharging =
                BatteryStatus.getChargingSpeed(context, batteryBroadcast)
                        == BatteryStatus.CHARGING_FAST;
        if (info.isBatteryDefender) {
            info.isBatteryDefender =
                    FeatureFactory.getFeatureFactory()
                            .getPowerUsageFeatureProvider()
                            .isBatteryDefend(info);
        }
        if (!info.mCharging) {
            updateBatteryInfoDischarging(context, shortString, estimate, info);
        } else {
            updateBatteryInfoCharging(
                    context,
                    batteryBroadcast,
                    batteryUsageStats,
                    info,
                    isCompactStatus,
                    currentTimeMs);
        }
        BatteryUtils.logRuntime(LOG_TAG, "time for getBatteryInfo", currentTimeMs);
        return info;
    }

    /** Returns a {@code BatteryInfo} with battery and charging relative information. */
    @WorkerThread
    public static BatteryInfo getBatteryInfo(
            Context context,
            Intent batteryBroadcast,
            BatteryUsageStats batteryUsageStats,
            Estimate estimate,
            long elapsedRealtimeUs,
            boolean shortString) {
        long currentTimeMs = System.currentTimeMillis();
        return getBatteryInfo(
                context,
                batteryBroadcast,
                batteryUsageStats,
                estimate,
                elapsedRealtimeUs,
                shortString,
                currentTimeMs);
    }

    private static void updateBatteryInfoCharging(
            Context context,
            Intent batteryBroadcast,
            BatteryUsageStats stats,
            BatteryInfo info,
            boolean compactStatus,
            long currentTimeMs) {
        final Resources resources = context.getResources();
        final long chargeTimeMs = stats.getChargeTimeRemainingMs();
        if (getSettingsChargeTimeRemaining(context) != chargeTimeMs) {
            Settings.Global.putLong(
                    context.getContentResolver(),
                    com.android.settingslib.fuelgauge.BatteryUtils.GLOBAL_TIME_TO_FULL_MILLIS,
                    chargeTimeMs);
        }

        final int status =
                batteryBroadcast.getIntExtra(
                        BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_UNKNOWN);
        info.discharging = false;
        info.suggestionLabel = null;
        int dockDefenderMode = BatteryUtils.getCurrentDockDefenderMode(context, info);
        if ((info.isBatteryDefender
                        && status != BatteryManager.BATTERY_STATUS_FULL
                        && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
                || dockDefenderMode == BatteryUtils.DockDefenderMode.ACTIVE) {
            // Battery defender active, battery charging paused
            info.remainingLabel = null;
            int chargingLimitedResId = R.string.power_charging_limited;
            info.chargeLabel = context.getString(chargingLimitedResId, info.batteryPercentString);
            return;
        }
        final BatterySettingsFeatureProvider featureProvider =
                FeatureFactory.getFeatureFactory().getBatterySettingsFeatureProvider();
        if (featureProvider.isChargingOptimizationMode(context)) {
            final CharSequence chargeLabel =
                    featureProvider.getChargingOptimizationChargeLabel(
                            context,
                            info.batteryLevel,
                            info.batteryPercentString,
                            chargeTimeMs,
                            currentTimeMs);
            if (chargeLabel != null) {
                final CharSequence remainingLabel =
                        featureProvider.getChargingOptimizationRemainingLabel(
                                context,
                                info.batteryLevel,
                                info.pluggedStatus,
                                chargeTimeMs,
                                currentTimeMs);
                if (remainingLabel != null) {
                    info.chargeLabel = chargeLabel;
                    info.remainingLabel = remainingLabel;
                    return;
                }
            }
        }
        if ((chargeTimeMs > 0
                        && status != BatteryManager.BATTERY_STATUS_FULL
                        && dockDefenderMode == BatteryUtils.DockDefenderMode.DISABLED)
                || dockDefenderMode == BatteryUtils.DockDefenderMode.TEMPORARILY_BYPASSED) {
            // Battery is charging to full
            info.remainingTimeUs = PowerUtil.convertMsToUs(chargeTimeMs);
            int resId = getChargingDurationResId(info.isFastCharging);
            info.remainingLabel =
                    chargeTimeMs <= 0
                            ? null
                            : getPowerRemainingChargingLabel(
                                    context,
                                    chargeTimeMs,
                                    info.isFastCharging,
                                    info.pluggedStatus,
                                    currentTimeMs,
                                    featureProvider);

            info.chargeLabel =
                    chargeTimeMs <= 0
                            ? info.batteryPercentString
                            : getChargeLabelWithTimeToFull(
                                    context,
                                    resId,
                                    info.batteryPercentString,
                                    chargeTimeMs,
                                    info.isFastCharging,
                                    currentTimeMs);
        } else if (dockDefenderMode == BatteryUtils.DockDefenderMode.FUTURE_BYPASS) {
            // Dock defender will be triggered in the future, charging will be optimized.
            info.chargeLabel =
                    context.getString(
                            R.string.power_charging_future_paused, info.batteryPercentString);
        } else {
            final String chargeStatusLabel =
                    Utils.getBatteryStatus(context, batteryBroadcast, compactStatus);
            info.remainingLabel = null;
            info.chargeLabel =
                    info.batteryLevel == 100
                            ? info.batteryPercentString
                            : resources.getString(
                                    R.string.power_charging,
                                    info.batteryPercentString,
                                    chargeStatusLabel);
        }
    }

    private static CharSequence getPowerRemainingChargingLabel(
            Context context,
            long chargeRemainingTimeMs,
            boolean isFastCharging,
            int pluggedStatus,
            long currentTimeMs,
            BatterySettingsFeatureProvider featureProvider) {
        if (pluggedStatus == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
            final CharSequence wirelessChargingRemainingLabel =
                    featureProvider.getWirelessChargingRemainingLabel(
                            context, chargeRemainingTimeMs, currentTimeMs);
            if (wirelessChargingRemainingLabel != null) {
                return wirelessChargingRemainingLabel;
            }
        }
        if (com.android.settingslib.fuelgauge.BatteryUtils.isChargingStringV2Enabled()) {
            int chargeLabelResId =
                    isFastCharging
                            ? R.string.power_remaining_fast_charging_duration_only_v2
                            : R.string.power_remaining_charging_duration_only_v2;
            String timeString =
                    PowerUtil.getTargetTimeShortString(
                            context, chargeRemainingTimeMs, currentTimeMs);
            return context.getString(chargeLabelResId, timeString);
        }
        final CharSequence timeString =
                StringUtil.formatElapsedTime(
                        context,
                        chargeRemainingTimeMs,
                        /* withSeconds= */ false,
                        /* collapseTimeUnit= */ true);
        return context.getString(R.string.power_remaining_charging_duration_only, timeString);
    }

    private static CharSequence getChargeLabelWithTimeToFull(
            Context context,
            int chargeLabelResId,
            String batteryPercentString,
            long chargeTimeMs,
            boolean isFastCharging,
            long currentTimeMs) {
        if (com.android.settingslib.fuelgauge.BatteryUtils.isChargingStringV2Enabled()) {
            var timeString =
                    PowerUtil.getTargetTimeShortString(context, chargeTimeMs, currentTimeMs);

            return isFastCharging
                    ? context.getString(
                            chargeLabelResId,
                            batteryPercentString,
                            context.getString(R.string.battery_info_status_charging_fast_v2),
                            timeString)
                    : context.getString(chargeLabelResId, batteryPercentString, timeString);
        } else {
            var timeString =
                    StringUtil.formatElapsedTime(
                            context,
                            (double) chargeTimeMs,
                            /* withSeconds= */ false,
                            /* collapseTimeUnit= */ true);
            return context.getString(chargeLabelResId, batteryPercentString, timeString);
        }
    }

    private static int getChargingDurationResId(boolean isFastCharging) {
        if (com.android.settingslib.fuelgauge.BatteryUtils.isChargingStringV2Enabled()) {
            return isFastCharging
                    ? R.string.power_fast_charging_duration_v2
                    : R.string.power_charging_duration_v2;
        }
        return R.string.power_charging_duration;
    }

    private static void updateBatteryInfoDischarging(
            Context context, boolean shortString, Estimate estimate, BatteryInfo info) {
        final long drainTimeUs = PowerUtil.convertMsToUs(estimate.getEstimateMillis());
        if (drainTimeUs > 0) {
            info.remainingTimeUs = drainTimeUs;
            info.remainingLabel =
                    PowerUtil.getBatteryRemainingShortStringFormatted(
                            context, PowerUtil.convertUsToMs(drainTimeUs));
            info.chargeLabel = info.remainingLabel;
            info.suggestionLabel =
                    PowerUtil.getBatteryTipStringFormatted(
                            context, PowerUtil.convertUsToMs(drainTimeUs));
        } else {
            info.remainingLabel = null;
            info.suggestionLabel = null;
            info.chargeLabel = info.batteryPercentString;
        }
    }

    public interface BatteryDataParser {
        void onParsingStarted(long startTime, long endTime);

        void onDataPoint(long time, HistoryItem record);

        void onDataGap();

        void onParsingDone();
    }

    /**
     * Iterates over battery history included in the BatteryUsageStats that this object was
     * initialized with.
     */
    public void parseBatteryHistory(BatteryDataParser... parsers) {
        long startWalltime = 0;
        long endWalltime = 0;
        long historyStart = 0;
        long historyEnd = 0;
        long curWalltime = startWalltime;
        long lastWallTime = 0;
        long lastRealtime = 0;
        int lastInteresting = 0;
        int pos = 0;
        boolean first = true;
        final BatteryStatsHistoryIterator iterator1 =
                mBatteryUsageStats.iterateBatteryStatsHistory();
        HistoryItem rec;
        while ((rec = iterator1.next()) != null) {
            pos++;
            if (first) {
                first = false;
                historyStart = rec.time;
            }
            if (rec.cmd == HistoryItem.CMD_CURRENT_TIME || rec.cmd == HistoryItem.CMD_RESET) {
                // If there is a ridiculously large jump in time, then we won't be
                // able to create a good chart with that data, so just ignore the
                // times we got before and pretend like our data extends back from
                // the time we have now.
                // Also, if we are getting a time change and we are less than 5 minutes
                // since the start of the history real time, then also use this new
                // time to compute the base time, since whatever time we had before is
                // pretty much just noise.
                if (rec.currentTime > (lastWallTime + (180 * 24 * 60 * 60 * 1000L))
                        || rec.time < (historyStart + (5 * 60 * 1000L))) {
                    startWalltime = 0;
                }
                lastWallTime = rec.currentTime;
                lastRealtime = rec.time;
                if (startWalltime == 0) {
                    startWalltime = lastWallTime - (lastRealtime - historyStart);
                }
            }
            if (rec.isDeltaData()) {
                lastInteresting = pos;
                historyEnd = rec.time;
            }
        }

        endWalltime = lastWallTime + historyEnd - lastRealtime;

        int i = 0;
        final int N = lastInteresting;

        for (int j = 0; j < parsers.length; j++) {
            parsers[j].onParsingStarted(startWalltime, endWalltime);
        }

        if (endWalltime > startWalltime) {
            final BatteryStatsHistoryIterator iterator2 =
                    mBatteryUsageStats.iterateBatteryStatsHistory();
            while ((rec = iterator2.next()) != null && i < N) {
                if (rec.isDeltaData()) {
                    curWalltime += rec.time - lastRealtime;
                    lastRealtime = rec.time;
                    long x = (curWalltime - startWalltime);
                    if (x < 0) {
                        x = 0;
                    }
                    for (int j = 0; j < parsers.length; j++) {
                        parsers[j].onDataPoint(x, rec);
                    }
                } else {
                    long lastWalltime = curWalltime;
                    if (rec.cmd == HistoryItem.CMD_CURRENT_TIME
                            || rec.cmd == HistoryItem.CMD_RESET) {
                        if (rec.currentTime >= startWalltime) {
                            curWalltime = rec.currentTime;
                        } else {
                            curWalltime = startWalltime + (rec.time - historyStart);
                        }
                        lastRealtime = rec.time;
                    }

                    if (rec.cmd != HistoryItem.CMD_OVERFLOW
                            && (rec.cmd != HistoryItem.CMD_CURRENT_TIME
                                    || Math.abs(lastWalltime - curWalltime) > (60 * 60 * 1000))) {
                        for (int j = 0; j < parsers.length; j++) {
                            parsers[j].onDataGap();
                        }
                    }
                }
                i++;
            }
        }

        for (int j = 0; j < parsers.length; j++) {
            parsers[j].onParsingDone();
        }
    }
}
