/*
 * Copyright (C) 2015 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.internal.os;

import android.os.BatteryConsumer;
import android.os.BatteryStats;
import android.os.BatteryUsageStats;
import android.os.BatteryUsageStatsQuery;
import android.os.UidBatteryConsumer;
import android.os.UserHandle;
import android.telephony.CellSignalStrength;
import android.util.Log;
import android.util.SparseArray;

import java.util.List;

public class MobileRadioPowerCalculator extends PowerCalculator {
    private static final String TAG = "MobRadioPowerCalculator";
    private static final boolean DEBUG = BatteryStatsHelper.DEBUG;

    private static final int NUM_SIGNAL_STRENGTH_LEVELS =
            CellSignalStrength.getNumSignalStrengthLevels();

    private final UsageBasedPowerEstimator mActivePowerEstimator;
    private final UsageBasedPowerEstimator[] mIdlePowerEstimators =
            new UsageBasedPowerEstimator[NUM_SIGNAL_STRENGTH_LEVELS];
    private final UsageBasedPowerEstimator mScanPowerEstimator;

    private static class PowerAndDuration {
        public long durationMs;
        public double remainingPowerMah;
        public long totalAppDurationMs;
        public double totalAppPowerMah;
        public long signalDurationMs;
        public long noCoverageDurationMs;
    }

    public MobileRadioPowerCalculator(PowerProfile profile) {
        // Power consumption when radio is active
        double powerRadioActiveMa =
                profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ACTIVE, -1);
        if (powerRadioActiveMa == -1) {
            double sum = 0;
            sum += profile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX);
            for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
                sum += profile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_TX, i);
            }
            powerRadioActiveMa = sum / (NUM_SIGNAL_STRENGTH_LEVELS + 1);
        }

        mActivePowerEstimator = new UsageBasedPowerEstimator(powerRadioActiveMa);

        // Power consumption when radio is on, but idle
        if (profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_ON, -1) != -1) {
            for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
                mIdlePowerEstimators[i] = new UsageBasedPowerEstimator(
                        profile.getAveragePower(PowerProfile.POWER_RADIO_ON, i));
            }
        } else {
            double idle = profile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_IDLE);

            // Magical calculations preserved for historical compatibility
            mIdlePowerEstimators[0] = new UsageBasedPowerEstimator(idle * 25 / 180);
            for (int i = 1; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
                mIdlePowerEstimators[i] = new UsageBasedPowerEstimator(Math.max(1, idle / 256));
            }
        }

        mScanPowerEstimator = new UsageBasedPowerEstimator(
                profile.getAveragePowerOrDefault(PowerProfile.POWER_RADIO_SCANNING, 0));
    }

    @Override
    public void calculate(BatteryUsageStats.Builder builder, BatteryStats batteryStats,
            long rawRealtimeUs, long rawUptimeUs, BatteryUsageStatsQuery query) {

        PowerAndDuration total = new PowerAndDuration();

        final double powerPerPacketMah = getMobilePowerPerPacket(batteryStats, rawRealtimeUs,
                BatteryStats.STATS_SINCE_CHARGED);
        final SparseArray<UidBatteryConsumer.Builder> uidBatteryConsumerBuilders =
                builder.getUidBatteryConsumerBuilders();
        for (int i = uidBatteryConsumerBuilders.size() - 1; i >= 0; i--) {
            final UidBatteryConsumer.Builder app = uidBatteryConsumerBuilders.valueAt(i);
            final BatteryStats.Uid uid = app.getBatteryStatsUid();
            calculateApp(app, uid, powerPerPacketMah, total, query);
        }

        final long totalConsumptionUC = batteryStats.getMobileRadioMeasuredBatteryConsumptionUC();
        final int powerModel = getPowerModel(totalConsumptionUC, query);
        calculateRemaining(total, powerModel, batteryStats, rawRealtimeUs, totalConsumptionUC);

        if (total.remainingPowerMah != 0 || total.totalAppPowerMah != 0) {
            builder.getAggregateBatteryConsumerBuilder(
                    BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_DEVICE)
                    .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                            total.durationMs)
                    .setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                            total.remainingPowerMah + total.totalAppPowerMah, powerModel);

            builder.getAggregateBatteryConsumerBuilder(
                    BatteryUsageStats.AGGREGATE_BATTERY_CONSUMER_SCOPE_ALL_APPS)
                    .setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                            total.durationMs)
                    .setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                            total.totalAppPowerMah, powerModel);
        }
    }

    private void calculateApp(UidBatteryConsumer.Builder app, BatteryStats.Uid u,
            double powerPerPacketMah, PowerAndDuration total,
            BatteryUsageStatsQuery query) {
        final long radioActiveDurationMs = calculateDuration(u, BatteryStats.STATS_SINCE_CHARGED);
        total.totalAppDurationMs += radioActiveDurationMs;

        final long consumptionUC = u.getMobileRadioMeasuredBatteryConsumptionUC();
        final int powerModel = getPowerModel(consumptionUC, query);
        final double powerMah = calculatePower(u, powerModel, powerPerPacketMah,
                radioActiveDurationMs, consumptionUC);
        total.totalAppPowerMah += powerMah;

        app.setUsageDurationMillis(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
                        radioActiveDurationMs)
                .setConsumedPower(BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO, powerMah,
                        powerModel);
    }

    @Override
    public void calculate(List<BatterySipper> sippers, BatteryStats batteryStats,
            long rawRealtimeUs, long rawUptimeUs, int statsType, SparseArray<UserHandle> asUsers) {
        final double mobilePowerPerPacket = getMobilePowerPerPacket(batteryStats, rawRealtimeUs,
                statsType);
        PowerAndDuration total = new PowerAndDuration();
        for (int i = sippers.size() - 1; i >= 0; i--) {
            final BatterySipper app = sippers.get(i);
            if (app.drainType == BatterySipper.DrainType.APP) {
                final BatteryStats.Uid u = app.uidObj;
                calculateApp(app, u, statsType, mobilePowerPerPacket, total);
            }
        }

        BatterySipper radio = new BatterySipper(BatterySipper.DrainType.CELL, null, 0);
        final long consumptionUC = batteryStats.getMobileRadioMeasuredBatteryConsumptionUC();
        final int powerModel = getPowerModel(consumptionUC);
        calculateRemaining(total, powerModel, batteryStats, rawRealtimeUs, consumptionUC);
        if (total.remainingPowerMah != 0) {
            if (total.signalDurationMs != 0) {
                radio.noCoveragePercent =
                        total.noCoverageDurationMs * 100.0 / total.signalDurationMs;
            }
            radio.mobileActive = total.durationMs;
            radio.mobileActiveCount = batteryStats.getMobileRadioActiveUnknownCount(statsType);
            radio.mobileRadioPowerMah = total.remainingPowerMah;
            radio.sumPower();
        }
        if (radio.totalPowerMah > 0) {
            sippers.add(radio);
        }
    }

    private void calculateApp(BatterySipper app, BatteryStats.Uid u, int statsType,
            double powerPerPacketMah, PowerAndDuration total) {
        app.mobileActive = calculateDuration(u, statsType);

        final long consumptionUC =  u.getMobileRadioMeasuredBatteryConsumptionUC();
        final int powerModel = getPowerModel(consumptionUC);
        app.mobileRadioPowerMah = calculatePower(u, powerModel, powerPerPacketMah, app.mobileActive,
                consumptionUC);
        total.totalAppDurationMs += app.mobileActive;

        // Add cost of mobile traffic.
        app.mobileRxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_RX_DATA,
                statsType);
        app.mobileTxPackets = u.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_TX_DATA,
                statsType);
        app.mobileActiveCount = u.getMobileRadioActiveCount(statsType);
        app.mobileRxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_MOBILE_RX_DATA,
                statsType);
        app.mobileTxBytes = u.getNetworkActivityBytes(BatteryStats.NETWORK_MOBILE_TX_DATA,
                statsType);

        if (DEBUG && app.mobileRadioPowerMah != 0) {
            Log.d(TAG, "UID " + u.getUid() + ": mobile packets "
                    + (app.mobileRxPackets + app.mobileTxPackets)
                    + " active time " + app.mobileActive
                    + " power=" + formatCharge(app.mobileRadioPowerMah));
        }
    }

    private long calculateDuration(BatteryStats.Uid u, int statsType) {
        return u.getMobileRadioActiveTime(statsType) / 1000;
    }

    private double calculatePower(BatteryStats.Uid u, @BatteryConsumer.PowerModel int powerModel,
            double powerPerPacketMah, long radioActiveDurationMs, long measuredChargeUC) {
        if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
            return uCtoMah(measuredChargeUC);
        }

        if (radioActiveDurationMs > 0) {
            // We are tracking when the radio is up, so can use the active time to
            // determine power use.
            return calcPowerFromRadioActiveDurationMah(radioActiveDurationMs);
        } else {
            // We are not tracking when the radio is up, so must approximate power use
            // based on the number of packets.
            final long mobileRxPackets = u.getNetworkActivityPackets(
                    BatteryStats.NETWORK_MOBILE_RX_DATA,
                    BatteryStats.STATS_SINCE_CHARGED);
            final long mobileTxPackets = u.getNetworkActivityPackets(
                    BatteryStats.NETWORK_MOBILE_TX_DATA,
                    BatteryStats.STATS_SINCE_CHARGED);
            return (mobileRxPackets + mobileTxPackets) * powerPerPacketMah;
        }
    }

    private void calculateRemaining(PowerAndDuration total,
            @BatteryConsumer.PowerModel int powerModel, BatteryStats batteryStats,
            long rawRealtimeUs, long totalConsumptionUC) {
        long signalTimeMs = 0;
        double powerMah = 0;

        if (powerModel == BatteryConsumer.POWER_MODEL_MEASURED_ENERGY) {
            powerMah = uCtoMah(totalConsumptionUC) - total.totalAppPowerMah;
            if (powerMah < 0) powerMah = 0;
        }

        for (int i = 0; i < NUM_SIGNAL_STRENGTH_LEVELS; i++) {
            long strengthTimeMs = batteryStats.getPhoneSignalStrengthTime(i, rawRealtimeUs,
                    BatteryStats.STATS_SINCE_CHARGED) / 1000;
            if (powerModel == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
                final double p = calcIdlePowerAtSignalStrengthMah(strengthTimeMs, i);
                if (DEBUG && p != 0) {
                    Log.d(TAG, "Cell strength #" + i + ": time=" + strengthTimeMs + " power="
                            + formatCharge(p));
                }
                powerMah += p;
            }
            signalTimeMs += strengthTimeMs;
            if (i == 0) {
                total.noCoverageDurationMs = strengthTimeMs;
            }
        }

        final long scanningTimeMs = batteryStats.getPhoneSignalScanningTime(rawRealtimeUs,
                BatteryStats.STATS_SINCE_CHARGED) / 1000;
        long radioActiveTimeMs = batteryStats.getMobileRadioActiveTime(rawRealtimeUs,
                BatteryStats.STATS_SINCE_CHARGED) / 1000;
        long remainingActiveTimeMs = radioActiveTimeMs - total.totalAppDurationMs;

        if (powerModel == BatteryConsumer.POWER_MODEL_POWER_PROFILE) {
            final double p = calcScanTimePowerMah(scanningTimeMs);
            if (DEBUG && p != 0) {
                Log.d(TAG, "Cell radio scanning: time=" + scanningTimeMs + " power=" + formatCharge(
                        p));
            }
            powerMah += p;

            if (remainingActiveTimeMs > 0) {
                powerMah += calcPowerFromRadioActiveDurationMah(remainingActiveTimeMs);
            }
        }
        total.durationMs = radioActiveTimeMs;
        total.remainingPowerMah = powerMah;
        total.signalDurationMs = signalTimeMs;
    }

    /**
     * Calculates active radio power consumption (in milliamp-hours) from active radio duration.
     */
    public double calcPowerFromRadioActiveDurationMah(long radioActiveDurationMs) {
        return mActivePowerEstimator.calculatePower(radioActiveDurationMs);
    }

    /**
     * Calculates idle radio power consumption (in milliamp-hours) for time spent at a cell signal
     * strength level.
     * see {@link CellSignalStrength#getNumSignalStrengthLevels()}
     */
    public double calcIdlePowerAtSignalStrengthMah(long strengthTimeMs, int strengthLevel) {
        return mIdlePowerEstimators[strengthLevel].calculatePower(strengthTimeMs);
    }

    /**
     * Calculates radio scan power consumption (in milliamp-hours) from scan time.
     */
    public double calcScanTimePowerMah(long scanningTimeMs) {
        return mScanPowerEstimator.calculatePower(scanningTimeMs);
    }

    /**
     * Return estimated power (in mAh) of sending or receiving a packet with the mobile radio.
     */
    private double getMobilePowerPerPacket(BatteryStats stats, long rawRealtimeUs, int statsType) {
        final long radioDataUptimeMs =
                stats.getMobileRadioActiveTime(rawRealtimeUs, statsType) / 1000;
        final double mobilePower = calcPowerFromRadioActiveDurationMah(radioDataUptimeMs);

        final long mobileRx = stats.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_RX_DATA,
                statsType);
        final long mobileTx = stats.getNetworkActivityPackets(BatteryStats.NETWORK_MOBILE_TX_DATA,
                statsType);
        final long mobilePackets = mobileRx + mobileTx;

        return mobilePackets != 0 ? mobilePower / mobilePackets : 0;
    }
}
