/**
 * Copyright (C) 2009 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.telephony;

import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.Context;
import android.os.PersistableBundle;
import android.os.SystemProperties;
import android.telephony.CarrierConfigManager;
import android.telephony.data.ApnSetting;
import android.text.TextUtils;
import android.util.Pair;

import com.android.internal.telephony.util.TelephonyUtils;
import com.android.telephony.Rlog;

import java.util.ArrayList;
import java.util.Random;

/**
 * Retry manager allows a simple way to declare a series of
 * retry timeouts. After creating a RetryManager the configure
 * method is used to define the sequence. A simple linear series
 * may be initialized using configure with three integer parameters
 * The other configure method allows a series to be declared using
 * a string.
 *<p>
 * The format of the configuration string is the apn type followed by a series of parameters
 * separated by a comma. There are two name value pair parameters plus a series
 * of delay times. The units of of these delay times is unspecified.
 * The name value pairs which may be specified are:
 *<ul>
 *<li>max_retries=<value>
 *<li>default_randomizationTime=<value>
 *</ul>
 *<p>
 * apn type specifies the APN type that the retry pattern will apply for. "others" is for all other
 * APN types not specified in the config.
 *
 * max_retries is the number of times that incrementRetryCount
 * maybe called before isRetryNeeded will return false. if value
 * is infinite then isRetryNeeded will always return true.
 *
 * default_randomizationTime will be used as the randomizationTime
 * for delay times which have no supplied randomizationTime. If
 * default_randomizationTime is not defined it defaults to 0.
 *<p>
 * The other parameters define The series of delay times and each
 * may have an optional randomization value separated from the
 * delay time by a colon.
 *<p>
 * Examples:
 * <ul>
 * <li>3 retries for mms with no randomization value which means its 0:
 * <ul><li><code>"mms:1000, 2000, 3000"</code></ul>
 *
 * <li>10 retries for default APN with a 500 default randomization value for each and
 * the 4..10 retries all using 3000 as the delay:
 * <ul><li><code>"default:max_retries=10, default_randomization=500, 1000, 2000, 3000"</code></ul>
 *
 * <li>4 retries for supl APN with a 100 as the default randomization value for the first 2 values
 * and the other two having specified values of 500:
 * <ul><li><code>"supl:default_randomization=100, 1000, 2000, 4000:500, 5000:500"</code></ul>
 *
 * <li>Infinite number of retries for all other APNs with the first one at 1000, the second at 2000
 * all others will be at 3000.
 * <ul><li><code>"others:max_retries=infinite,1000,2000,3000</code></ul>
 * </ul>
 *
 * {@hide}
 */
public class RetryManager {
    public static final String LOG_TAG = "RetryManager";
    public static final boolean DBG = true;
    public static final boolean VDBG = false; // STOPSHIP if true

    /**
     * The default retry configuration for APNs. See above for the syntax.
     */
    private static final String DEFAULT_DATA_RETRY_CONFIG = "max_retries=3, 5000, 5000, 5000";

    /**
     * The APN type used for all other APNs retry configuration.
     */
    private static final String OTHERS_APN_TYPE = "others";

    /**
     * The default value (in milliseconds) for delay between APN trying (mInterApnDelay)
     * within the same round
     */
    private static final long DEFAULT_INTER_APN_DELAY = 20000;

    /**
     * The default value (in milliseconds) for delay between APN trying (mFailFastInterApnDelay)
     * within the same round when we are in fail fast mode
     */
    private static final long DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING = 3000;

    /**
     * The default value (in milliseconds) for retrying APN after disconnect
     */
    private static final long DEFAULT_APN_RETRY_AFTER_DISCONNECT_DELAY = 10000;

    /**
     * The value indicating no retry is needed
     */
    public static final long NO_RETRY = -1;

    /**
     * The value indicating modem did not suggest any retry delay
     */
    public static final long NO_SUGGESTED_RETRY_DELAY = -2;

    /**
     * If the modem suggests a retry delay in the data call setup response, we will retry
     * the current APN setting again. However, if the modem keeps suggesting retrying the same
     * APN setting, we'll fall into an infinite loop. Therefore adding a counter to retry up to
     * MAX_SAME_APN_RETRY times can avoid it.
     */
    private static final int MAX_SAME_APN_RETRY = 3;

    /**
     * The delay (in milliseconds) between APN trying within the same round
     */
    @UnsupportedAppUsage
    private long mInterApnDelay;

    /**
     * The delay (in milliseconds) between APN trying within the same round when we are in
     * fail fast mode
     */
    @UnsupportedAppUsage
    private long mFailFastInterApnDelay;

    /**
     * The delay (in milliseconds) for APN retrying after disconnect (e.g. Modem suddenly reports
     * data call lost)
     */
    private long mApnRetryAfterDisconnectDelay;

    /**
     * Modem suggested delay for retrying the current APN
     */
    private long mModemSuggestedDelay = NO_SUGGESTED_RETRY_DELAY;

    /**
     * The counter for same APN retrying. See MAX_SAME_APN_RETRY for the details.
     */
    private int mSameApnRetryCount = 0;

    /**
     * Retry record with times in milli-seconds
     */
    private static class RetryRec {
        RetryRec(int delayTime, int randomizationTime) {
            mDelayTime = delayTime;
            mRandomizationTime = randomizationTime;
        }

        int mDelayTime;
        int mRandomizationTime;
    }

    /**
     * The array of retry records
     */
    private ArrayList<RetryRec> mRetryArray = new ArrayList<RetryRec>();

    @UnsupportedAppUsage
    private Phone mPhone;

    /**
     * Flag indicating whether retrying forever regardless the maximum retry count mMaxRetryCount
     */
    private boolean mRetryForever = false;

    /**
     * The maximum number of retries to attempt
     */
    private int mMaxRetryCount;

    /**
     * The current number of retries
     */
    private int mRetryCount = 0;

    /**
     * Random number generator. The random delay will be added into retry timer to avoid all devices
     * around retrying the APN at the same time.
     */
    private Random mRng = new Random();

    /**
     * Retry manager configuration string. See top of the detailed explanation.
     */
    private String mConfig;

    /**
     * The list to store APN setting candidates for data call setup. Most of the carriers only have
     * one APN, but few carriers have more than one.
     */
    private ArrayList<ApnSetting> mWaitingApns = null;

    /**
     * Index pointing to the current trying APN from mWaitingApns
     */
    private int mCurrentApnIndex = -1;

    /**
     * Apn context type. Could be "default, "mms", "supl", etc...
     */
    @UnsupportedAppUsage
    private String mApnType;

    /**
     * Retry manager constructor
     * @param phone Phone object
     * @param apnType APN type
     */
    public RetryManager(Phone phone, String apnType) {
        mPhone = phone;
        mApnType = apnType;
    }

    /**
     * Configure for using string which allow arbitrary
     * sequences of times. See class comments for the
     * string format.
     *
     * @return true if successful
     */
    @UnsupportedAppUsage
    private boolean configure(String configStr) {
        // Strip quotes if present.
        if ((configStr.startsWith("\"") && configStr.endsWith("\""))) {
            configStr = configStr.substring(1, configStr.length() - 1);
        }

        // Reset the retry manager since delay, max retry count, etc...will be reset.
        reset();

        if (DBG) log("configure: '" + configStr + "'");
        mConfig = configStr;

        if (!TextUtils.isEmpty(configStr)) {
            int defaultRandomization = 0;

            if (VDBG) log("configure: not empty");

            String strArray[] = configStr.split(",");
            for (int i = 0; i < strArray.length; i++) {
                if (VDBG) log("configure: strArray[" + i + "]='" + strArray[i] + "'");
                Pair<Boolean, Integer> value;
                String splitStr[] = strArray[i].split("=", 2);
                splitStr[0] = splitStr[0].trim();
                if (VDBG) log("configure: splitStr[0]='" + splitStr[0] + "'");
                if (splitStr.length > 1) {
                    splitStr[1] = splitStr[1].trim();
                    if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'");
                    if (TextUtils.equals(splitStr[0], "default_randomization")) {
                        value = parseNonNegativeInt(splitStr[0], splitStr[1]);
                        if (!value.first) return false;
                        defaultRandomization = value.second;
                    } else if (TextUtils.equals(splitStr[0], "max_retries")) {
                        if (TextUtils.equals("infinite", splitStr[1])) {
                            mRetryForever = true;
                        } else {
                            value = parseNonNegativeInt(splitStr[0], splitStr[1]);
                            if (!value.first) return false;
                            mMaxRetryCount = value.second;
                        }
                    } else {
                        Rlog.e(LOG_TAG, "Unrecognized configuration name value pair: "
                                        + strArray[i]);
                        return false;
                    }
                } else {
                    /**
                     * Assume a retry time with an optional randomization value
                     * following a ":"
                     */
                    splitStr = strArray[i].split(":", 2);
                    splitStr[0] = splitStr[0].trim();
                    RetryRec rr = new RetryRec(0, 0);
                    value = parseNonNegativeInt("delayTime", splitStr[0]);
                    if (!value.first) return false;
                    rr.mDelayTime = value.second;

                    // Check if optional randomization value present
                    if (splitStr.length > 1) {
                        splitStr[1] = splitStr[1].trim();
                        if (VDBG) log("configure: splitStr[1]='" + splitStr[1] + "'");
                        value = parseNonNegativeInt("randomizationTime", splitStr[1]);
                        if (!value.first) return false;
                        rr.mRandomizationTime = value.second;
                    } else {
                        rr.mRandomizationTime = defaultRandomization;
                    }
                    mRetryArray.add(rr);
                }
            }
            if (mRetryArray.size() > mMaxRetryCount) {
                mMaxRetryCount = mRetryArray.size();
                if (VDBG) log("configure: setting mMaxRetryCount=" + mMaxRetryCount);
            }
        } else {
            log("configure: cleared");
        }

        if (VDBG) log("configure: true");
        return true;
    }

    /**
     * Configure the retry manager
     */
    private void configureRetry() {
        String configString = null;
        String otherConfigString = null;

        try {
            if (TelephonyUtils.IS_DEBUGGABLE) {
                // Using system properties is easier for testing from command line.
                String config = SystemProperties.get("test.data_retry_config");
                if (!TextUtils.isEmpty(config)) {
                    configure(config);
                    return;
                }
            }

            CarrierConfigManager configManager = (CarrierConfigManager)
                    mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
            PersistableBundle b = configManager.getConfigForSubId(mPhone.getSubId());

            mInterApnDelay = b.getLong(
                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_DELAY_DEFAULT_LONG,
                    DEFAULT_INTER_APN_DELAY);
            mFailFastInterApnDelay = b.getLong(
                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_DELAY_FASTER_LONG,
                    DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING);
            mApnRetryAfterDisconnectDelay = b.getLong(
                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_APN_RETRY_AFTER_DISCONNECT_LONG,
                    DEFAULT_APN_RETRY_AFTER_DISCONNECT_DELAY);

            // Load all retry patterns for all different APNs.
            String[] allConfigStrings = b.getStringArray(
                    CarrierConfigManager.KEY_CARRIER_DATA_CALL_RETRY_CONFIG_STRINGS);
            if (allConfigStrings != null) {
                for (String s : allConfigStrings) {
                    if (!TextUtils.isEmpty(s)) {
                        String splitStr[] = s.split(":", 2);
                        if (splitStr.length == 2) {
                            String apnType = splitStr[0].trim();
                            // Check if this retry pattern is for the APN we want.
                            if (apnType.equals(mApnType)) {
                                // Extract the config string. Note that an empty string is valid
                                // here, meaning no retry for the specified APN.
                                configString = splitStr[1];
                                break;
                            } else if (apnType.equals(OTHERS_APN_TYPE)) {
                                // Extract the config string. Note that an empty string is valid
                                // here, meaning no retry for all other APNs.
                                otherConfigString = splitStr[1];
                            }
                        }
                    }
                }
            }

            if (configString == null) {
                if (otherConfigString != null) {
                    configString = otherConfigString;
                } else {
                    // We should never reach here. If we reach here, it must be a configuration
                    // error bug.
                    log("Invalid APN retry configuration!. Use the default one now.");
                    configString = DEFAULT_DATA_RETRY_CONFIG;
                }
            }
        } catch (NullPointerException ex) {
            // We should never reach here unless there is a bug
            log("Failed to read configuration! Use the hardcoded default value.");

            mInterApnDelay = DEFAULT_INTER_APN_DELAY;
            mFailFastInterApnDelay = DEFAULT_INTER_APN_DELAY_FOR_PROVISIONING;
            configString = DEFAULT_DATA_RETRY_CONFIG;
        }

        if (VDBG) {
            log("mInterApnDelay = " + mInterApnDelay + ", mFailFastInterApnDelay = " +
                    mFailFastInterApnDelay);
        }

        configure(configString);
    }

    /**
     * Return the timer that should be used to trigger the data reconnection
     */
    @UnsupportedAppUsage
    private int getRetryTimer() {
        int index;
        if (mRetryCount < mRetryArray.size()) {
            index = mRetryCount;
        } else {
            index = mRetryArray.size() - 1;
        }

        int retVal;
        if ((index >= 0) && (index < mRetryArray.size())) {
            retVal = mRetryArray.get(index).mDelayTime + nextRandomizationTime(index);
        } else {
            retVal = 0;
        }

        if (DBG) log("getRetryTimer: " + retVal);
        return retVal;
    }

    /**
     * Parse an integer validating the value is not negative.
     * @param name Name
     * @param stringValue Value
     * @return Pair.first == true if stringValue an integer >= 0
     */
    private Pair<Boolean, Integer> parseNonNegativeInt(String name, String stringValue) {
        int value;
        Pair<Boolean, Integer> retVal;
        try {
            value = Integer.parseInt(stringValue);
            retVal = new Pair<Boolean, Integer>(validateNonNegativeInt(name, value), value);
        } catch (NumberFormatException e) {
            Rlog.e(LOG_TAG, name + " bad value: " + stringValue, e);
            retVal = new Pair<Boolean, Integer>(false, 0);
        }
        if (VDBG) {
            log("parseNonNetativeInt: " + name + ", " + stringValue + ", "
                    + retVal.first + ", " + retVal.second);
        }
        return retVal;
    }

    /**
     * Validate an integer is >= 0 and logs an error if not
     * @param name Name
     * @param value Value
     * @return Pair.first
     */
    private boolean validateNonNegativeInt(String name, int value) {
        boolean retVal;
        if (value < 0) {
            Rlog.e(LOG_TAG, name + " bad value: is < 0");
            retVal = false;
        } else {
            retVal = true;
        }
        if (VDBG) log("validateNonNegative: " + name + ", " + value + ", " + retVal);
        return retVal;
    }

    /**
     * Return next random number for the index
     * @param index Retry index
     */
    private int nextRandomizationTime(int index) {
        int randomTime = mRetryArray.get(index).mRandomizationTime;
        if (randomTime == 0) {
            return 0;
        } else {
            return mRng.nextInt(randomTime);
        }
    }

    /**
     * Get the next APN setting for data call setup.
     * @return APN setting to try. {@code null} if cannot find any APN,
     */
    public @Nullable ApnSetting getNextApnSetting() {
        if (mWaitingApns == null || mWaitingApns.size() == 0) {
            log("Waiting APN list is null or empty.");
            return null;
        }

        // If the modem had suggested a retry delay, we should retry the current APN again
        // (up to MAX_SAME_APN_RETRY times) instead of getting the next APN setting from
        // our own list. If the APN waiting list has been reset before a setup data responses
        // arrive (i.e. mCurrentApnIndex=-1), then ignore the modem suggested retry.
        if (mCurrentApnIndex != -1 && mModemSuggestedDelay != NO_SUGGESTED_RETRY_DELAY
                && mSameApnRetryCount < MAX_SAME_APN_RETRY) {
            mSameApnRetryCount++;
            return mWaitingApns.get(mCurrentApnIndex);
        }

        mSameApnRetryCount = 0;

        int index = mCurrentApnIndex;
        // Loop through the APN list to find out the index of next non-permanent failed APN.
        while (true) {
            if (++index == mWaitingApns.size()) index = 0;

            // Stop if we find the non-failed APN.
            if (!mWaitingApns.get(index).getPermanentFailed()) {
                break;
            }

            // If all APNs have permanently failed, bail out.
            if (mWaitingApns.stream().allMatch(ApnSetting::getPermanentFailed)) {
                return null;
            }
        }

        mCurrentApnIndex = index;
        return mWaitingApns.get(mCurrentApnIndex);
    }

    /**
     * Get the delay for trying the next waiting APN from the list.
     * @param failFastEnabled True if fail fast mode enabled. In this case we'll use a shorter
     *                        delay.
     * @return delay in milliseconds
     */
    public long getDelayForNextApn(boolean failFastEnabled) {

        if (mWaitingApns == null || mWaitingApns.size() == 0) {
            log("Waiting APN list is null or empty.");
            return NO_RETRY;
        }

        if (mModemSuggestedDelay == NO_RETRY) {
            log("Modem suggested not retrying.");
            return NO_RETRY;
        }

        if (mModemSuggestedDelay != NO_SUGGESTED_RETRY_DELAY &&
                mSameApnRetryCount < MAX_SAME_APN_RETRY) {
            // If the modem explicitly suggests a retry delay, we should use it, even in fail fast
            // mode.
            log("Modem suggested retry in " + mModemSuggestedDelay + " ms.");
            return mModemSuggestedDelay;
        }

        // In order to determine the delay to try next APN, we need to peek the next available APN.
        // Case 1 - If we will start the next round of APN trying,
        //    we use the exponential-growth delay. (e.g. 5s, 10s, 30s...etc.)
        // Case 2 - If we are still within the same round of APN trying,
        //    we use the fixed standard delay between APNs. (e.g. 20s)

        int index = mCurrentApnIndex;
        while (true) {
            if (++index >= mWaitingApns.size()) index = 0;

            // Stop if we find the non-failed APN.
            if (!mWaitingApns.get(index).getPermanentFailed()) {
                break;
            }

            // If all APNs have permanently failed, bail out.
            if (mWaitingApns.stream().allMatch(ApnSetting::getPermanentFailed)) {
                log("All APNs have permanently failed.");
                return NO_RETRY;
            }
        }

        long delay;
        if (index <= mCurrentApnIndex) {
            // Case 1, if the next APN is in the next round.
            if (!mRetryForever && mRetryCount + 1 > mMaxRetryCount) {
                log("Reached maximum retry count " + mMaxRetryCount + ".");
                return NO_RETRY;
            }
            delay = getRetryTimer();
            ++mRetryCount;
        } else {
            // Case 2, if the next APN is still in the same round.
            delay = mInterApnDelay;
        }

        if (failFastEnabled && delay > mFailFastInterApnDelay) {
            // If we enable fail fast mode, and the delay we got is longer than
            // fail-fast delay (mFailFastInterApnDelay), use the fail-fast delay.
            // If the delay we calculated is already shorter than fail-fast delay,
            // then ignore fail-fast delay.
            delay = mFailFastInterApnDelay;
        }

        return delay;
    }

    /**
     * Mark the APN setting permanently failed.
     * @param apn APN setting to be marked as permanently failed
     * */
    public void markApnPermanentFailed(ApnSetting apn) {
        if (apn != null) {
            apn.setPermanentFailed(true);
        }
    }

    /**
     * Reset the retry manager.
     */
    private void reset() {
        mMaxRetryCount = 0;
        mRetryCount = 0;
        mCurrentApnIndex = -1;
        mSameApnRetryCount = 0;
        mModemSuggestedDelay = NO_SUGGESTED_RETRY_DELAY;
        mRetryArray.clear();
    }

    /**
     * Set waiting APNs for retrying in case needed.
     * @param waitingApns Waiting APN list
     */
    public void setWaitingApns(ArrayList<ApnSetting> waitingApns) {

        if (waitingApns == null) {
            log("No waiting APNs provided");
            return;
        }

        mWaitingApns = waitingApns;

        // Since we replace the entire waiting APN list, we need to re-config this retry manager.
        configureRetry();

        for (ApnSetting apn : mWaitingApns) {
            apn.setPermanentFailed(false);
        }

        log("Setting " + mWaitingApns.size() + " waiting APNs.");

        if (VDBG) {
            for (int i = 0; i < mWaitingApns.size(); i++) {
                log("  [" + i + "]:" + mWaitingApns.get(i));
            }
        }
    }

    /**
     * Get the list of waiting APNs.
     * @return the list of waiting APNs
     */
    public ArrayList<ApnSetting> getWaitingApns() {
        return mWaitingApns;
    }

    /**
     * Save the modem suggested delay for retrying the current APN.
     * This method is called when we get the suggested delay from RIL.
     * @param delay The delay in milliseconds
     */
    public void setModemSuggestedDelay(long delay) {
        if (mCurrentApnIndex == -1) {
            log("Waiting APN list has been reset. Ignore the value from modem.");
            return;
        }
        mModemSuggestedDelay = delay;
    }

    /**
     * Get the delay in milliseconds for APN retry after disconnect
     * @return The delay in milliseconds
     */
    public long getRetryAfterDisconnectDelay() {
        return mApnRetryAfterDisconnectDelay;
    }

    public String toString() {
        if (mConfig == null) return "";
        return "RetryManager: mApnType=" + mApnType + " mRetryCount=" + mRetryCount
                + " mMaxRetryCount=" + mMaxRetryCount + " mCurrentApnIndex=" + mCurrentApnIndex
                + " mSameApnRtryCount=" + mSameApnRetryCount + " mModemSuggestedDelay="
                + mModemSuggestedDelay + " mRetryForever=" + mRetryForever + " mInterApnDelay="
                + mInterApnDelay + " mApnRetryAfterDisconnectDelay=" + mApnRetryAfterDisconnectDelay
                + " mConfig={" + mConfig + "}";
    }

    @UnsupportedAppUsage
    private void log(String s) {
        Rlog.d(LOG_TAG, "[" + mApnType + "] " + s);
    }
}
