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

import android.app.PendingIntent;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.NetworkCapabilities;
import android.net.NetworkConfig;
import android.net.NetworkRequest;
import android.telephony.Rlog;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.SparseIntArray;

import com.android.internal.R;
import com.android.internal.telephony.DctConstants;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.RetryManager;
import com.android.internal.util.IndentingPrintWriter;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Maintain the Apn context
 */
public class ApnContext {

    public final String LOG_TAG;
    private final static String SLOG_TAG = "ApnContext";

    protected static final boolean DBG = false;

    private final Phone mPhone;

    private final String mApnType;

    private DctConstants.State mState;

    public final int priority;

    private ApnSetting mApnSetting;

    DcAsyncChannel mDcAc;

    String mReason;

    PendingIntent mReconnectAlarmIntent;

    /**
     * user/app requested connection on this APN
     */
    AtomicBoolean mDataEnabled;

    private final Object mRefCountLock = new Object();
    private int mRefCount = 0;

    /**
     * carrier requirements met
     */
    AtomicBoolean mDependencyMet;

    private final DcTracker mDcTracker;

    /**
     * Remember this as a change in this value to a more permissive state
     * should cause us to retry even permanent failures
     */
    private boolean mConcurrentVoiceAndDataAllowed;

    /**
     * used to track a single connection request so disconnects can get ignored if
     * obsolete.
     */
    private final AtomicInteger mConnectionGeneration = new AtomicInteger(0);

    /**
     * Retry manager that handles the APN retry and delays.
     */
    private final RetryManager mRetryManager;

    /**
     * AonContext constructor
     * @param phone phone object
     * @param apnType APN type (e.g. default, supl, mms, etc...)
     * @param logTag Tag for logging
     * @param config Network configuration
     * @param tracker Data call tracker
     */
    public ApnContext(Phone phone, String apnType, String logTag, NetworkConfig config,
            DcTracker tracker) {
        mPhone = phone;
        mApnType = apnType;
        mState = DctConstants.State.IDLE;
        setReason(Phone.REASON_DATA_ENABLED);
        mDataEnabled = new AtomicBoolean(false);
        mDependencyMet = new AtomicBoolean(config.dependencyMet);
        priority = config.priority;
        LOG_TAG = logTag;
        mDcTracker = tracker;
        mRetryManager = new RetryManager(phone, apnType);
    }

    /**
     * Get the APN type
     * @return The APN type
     */
    public String getApnType() {
        return mApnType;
    }

    /**
     * Get the data call async channel.
     * @return The data call async channel
     */
    public synchronized DcAsyncChannel getDcAc() {
        return mDcAc;
    }

    /**
     * Set the data call async channel.
     * @param dcac The data call async channel
     */
    public synchronized void setDataConnectionAc(DcAsyncChannel dcac) {
        if (DBG) {
            log("setDataConnectionAc: old dcac=" + mDcAc + " new dcac=" + dcac
                    + " this=" + this);
        }
        mDcAc = dcac;
    }

    /**
     * Release data connection.
     * @param reason The reason of releasing data connection
     */
    public synchronized void releaseDataConnection(String reason) {
        if (mDcAc != null) {
            mDcAc.tearDown(this, reason, null);
            mDcAc = null;
        }
        setState(DctConstants.State.IDLE);
    }

    /**
     * Get the reconnect intent.
     * @return The reconnect intent
     */
    public synchronized PendingIntent getReconnectIntent() {
        return mReconnectAlarmIntent;
    }

    /**
     * Save the reconnect intent which can be used for cancelling later.
     * @param intent The reconnect intent
     */
    public synchronized void setReconnectIntent(PendingIntent intent) {
        mReconnectAlarmIntent = intent;
    }

    /**
     * Get the current APN setting.
     * @return APN setting
     */
    public synchronized ApnSetting getApnSetting() {
        if (DBG) log("getApnSetting: apnSetting=" + mApnSetting);
        return mApnSetting;
    }

    /**
     * Set the APN setting.
     * @param apnSetting APN setting
     */
    public synchronized void setApnSetting(ApnSetting apnSetting) {
        if (DBG) log("setApnSetting: apnSetting=" + apnSetting);
        mApnSetting = apnSetting;
    }

    /**
     * Set the list of APN candidates which will be used for data call setup later.
     * @param waitingApns List of APN candidates
     */
    public synchronized void setWaitingApns(ArrayList<ApnSetting> waitingApns) {
        mRetryManager.setWaitingApns(waitingApns);
    }

    /**
     * Get the next available APN to try.
     * @return APN setting which will be used for data call setup. Return null if there is no
     * APN can be retried.
     */
    public ApnSetting getNextApnSetting() {
        return mRetryManager.getNextApnSetting();
    }

    /**
     * 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) {
        mRetryManager.setModemSuggestedDelay(delay);
    }

    /**
     * Get the delay for trying the next APN setting if the current one failed.
     * @param failFastEnabled True if fail fast mode enabled. In this case we'll use a shorter
     *                        delay.
     * @return The delay in milliseconds
     */
    public long getDelayForNextApn(boolean failFastEnabled) {
        return mRetryManager.getDelayForNextApn(failFastEnabled);
    }

    /**
     * Mark the current APN setting permanently failed, which means it will not be retried anymore.
     * @param apn APN setting
     */
    public void markApnPermanentFailed(ApnSetting apn) {
        mRetryManager.markApnPermanentFailed(apn);
    }

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

    /**
     * Save the state indicating concurrent voice/data allowed.
     * @param allowed True if concurrent voice/data is allowed
     */
    public synchronized void setConcurrentVoiceAndDataAllowed(boolean allowed) {
        mConcurrentVoiceAndDataAllowed = allowed;
    }

    /**
     * Get the state indicating concurrent voice/data allowed.
     * @return True if concurrent voice/data is allowed
     */
    public synchronized boolean isConcurrentVoiceAndDataAllowed() {
        return mConcurrentVoiceAndDataAllowed;
    }

    /**
     * Set the current data call state.
     * @param s Current data call state
     */
    public synchronized void setState(DctConstants.State s) {
        if (DBG) {
            log("setState: " + s + ", previous state:" + mState);
        }

        mState = s;

        if (mState == DctConstants.State.FAILED) {
            if (mRetryManager.getWaitingApns() != null) {
                mRetryManager.getWaitingApns().clear(); // when teardown the connection and set to IDLE
            }
        }
    }

    /**
     * Get the current data call state.
     * @return The current data call state
     */
    public synchronized DctConstants.State getState() {
        return mState;
    }

    /**
     * Check whether the data call is disconnected or not.
     * @return True if the data call is disconnected
     */
    public boolean isDisconnected() {
        DctConstants.State currentState = getState();
        return ((currentState == DctConstants.State.IDLE) ||
                    currentState == DctConstants.State.FAILED);
    }

    /**
     * Set the reason for data call connection.
     * @param reason Reason for data call connection
     */
    public synchronized void setReason(String reason) {
        if (DBG) {
            log("set reason as " + reason + ",current state " + mState);
        }
        mReason = reason;
    }

    /**
     * Get the reason for data call connection.
     * @return The reason for data call connection
     */
    public synchronized String getReason() {
        return mReason;
    }

    /**
     * Check if ready for data call connection
     * @return True if ready, otherwise false.
     */
    public boolean isReady() {
        return mDataEnabled.get() && mDependencyMet.get();
    }

    /**
     * Check if the data call is in the state which allow connecting.
     * @return True if allowed, otherwise false.
     */
    public boolean isConnectable() {
        return isReady() && ((mState == DctConstants.State.IDLE)
                                || (mState == DctConstants.State.SCANNING)
                                || (mState == DctConstants.State.RETRYING)
                                || (mState == DctConstants.State.FAILED));
    }

    /** Check if the data call is in connected or connecting state.
     * @return True if the data call is in connected or connecting state
     */
    public boolean isConnectedOrConnecting() {
        return isReady() && ((mState == DctConstants.State.CONNECTED)
                                || (mState == DctConstants.State.CONNECTING)
                                || (mState == DctConstants.State.SCANNING)
                                || (mState == DctConstants.State.RETRYING));
    }

    /**
     * Set data call enabled/disabled state.
     * @param enabled True if data call is enabled
     */
    public void setEnabled(boolean enabled) {
        if (DBG) {
            log("set enabled as " + enabled + ", current state is " + mDataEnabled.get());
        }
        mDataEnabled.set(enabled);
    }

    /**
     * Check if the data call is enabled or not.
     * @return True if enabled
     */
    public boolean isEnabled() {
        return mDataEnabled.get();
    }

    public void setDependencyMet(boolean met) {
        if (DBG) {
            log("set mDependencyMet as " + met + " current state is " + mDependencyMet.get());
        }
        mDependencyMet.set(met);
    }

    public boolean getDependencyMet() {
       return mDependencyMet.get();
    }

    public boolean isProvisioningApn() {
        String provisioningApn = mPhone.getContext().getResources()
                .getString(R.string.mobile_provisioning_apn);
        if (!TextUtils.isEmpty(provisioningApn) &&
                (mApnSetting != null) && (mApnSetting.apn != null)) {
            return (mApnSetting.apn.equals(provisioningApn));
        } else {
            return false;
        }
    }

    private final ArrayList<LocalLog> mLocalLogs = new ArrayList<LocalLog>();
    private final ArrayDeque<LocalLog> mHistoryLogs = new ArrayDeque<LocalLog>();
    private final static int MAX_HISTORY_LOG_COUNT = 4;

    public void requestLog(String str) {
        synchronized (mRefCountLock) {
            for (LocalLog l : mLocalLogs) {
                l.log(str);
            }
        }
    }

    public void incRefCount(LocalLog log) {
        synchronized (mRefCountLock) {
            if (mLocalLogs.contains(log)) {
                log.log("ApnContext.incRefCount has duplicate add - " + mRefCount);
            } else {
                mLocalLogs.add(log);
                log.log("ApnContext.incRefCount - " + mRefCount);
            }
            if (mRefCount++ == 0) {
                mDcTracker.setEnabled(apnIdForApnName(mApnType), true);
            }
        }
    }

    public void decRefCount(LocalLog log) {
        synchronized (mRefCountLock) {
            if (mLocalLogs.remove(log)) {
                log.log("ApnContext.decRefCount - " + mRefCount);
                mHistoryLogs.addFirst(log);
                while (mHistoryLogs.size() > MAX_HISTORY_LOG_COUNT) {
                    mHistoryLogs.removeLast();
                }
            } else {
                log.log("ApnContext.decRefCount didn't find log - " + mRefCount);
            }
            if (mRefCount-- == 1) {
                mDcTracker.setEnabled(apnIdForApnName(mApnType), false);
            }
            if (mRefCount < 0) {
                log.log("ApnContext.decRefCount went to " + mRefCount);
                mRefCount = 0;
            }
        }
    }

    private final SparseIntArray mRetriesLeftPerErrorCode = new SparseIntArray();

    public void resetErrorCodeRetries() {
        requestLog("ApnContext.resetErrorCodeRetries");
        if (DBG) log("ApnContext.resetErrorCodeRetries");

        String[] config = mPhone.getContext().getResources().getStringArray(
                com.android.internal.R.array.config_cell_retries_per_error_code);
        synchronized (mRetriesLeftPerErrorCode) {
            mRetriesLeftPerErrorCode.clear();

            for (String c : config) {
                String errorValue[] = c.split(",");
                if (errorValue != null && errorValue.length == 2) {
                    int count = 0;
                    int errorCode = 0;
                    try {
                        errorCode = Integer.parseInt(errorValue[0]);
                        count = Integer.parseInt(errorValue[1]);
                    } catch (NumberFormatException e) {
                        log("Exception parsing config_retries_per_error_code: " + e);
                        continue;
                    }
                    if (count > 0 && errorCode > 0) {
                        mRetriesLeftPerErrorCode.put(errorCode, count);
                    }
                } else {
                    log("Exception parsing config_retries_per_error_code: " + c);
                }
            }
        }
    }

    public boolean restartOnError(int errorCode) {
        boolean result = false;
        int retriesLeft = 0;
        synchronized(mRetriesLeftPerErrorCode) {
            retriesLeft = mRetriesLeftPerErrorCode.get(errorCode);
            switch (retriesLeft) {
                case 0: {
                    // not set, never restart modem
                    break;
                }
                case 1: {
                    resetErrorCodeRetries();
                    result = true;
                    break;
                }
                default: {
                    mRetriesLeftPerErrorCode.put(errorCode, retriesLeft - 1);
                    result = false;
                }
            }
        }
        String str = "ApnContext.restartOnError(" + errorCode + ") found " + retriesLeft +
                " and returned " + result;
        if (DBG) log(str);
        requestLog(str);
        return result;
    }

    public int incAndGetConnectionGeneration() {
        return mConnectionGeneration.incrementAndGet();
    }

    public int getConnectionGeneration() {
        return mConnectionGeneration.get();
    }

    public long getInterApnDelay(boolean failFastEnabled) {
        return mRetryManager.getInterApnDelay(failFastEnabled);
    }

    public static int apnIdForType(int networkType) {
        switch (networkType) {
        case ConnectivityManager.TYPE_MOBILE:
            return DctConstants.APN_DEFAULT_ID;
        case ConnectivityManager.TYPE_MOBILE_MMS:
            return DctConstants.APN_MMS_ID;
        case ConnectivityManager.TYPE_MOBILE_SUPL:
            return DctConstants.APN_SUPL_ID;
        case ConnectivityManager.TYPE_MOBILE_DUN:
            return DctConstants.APN_DUN_ID;
        case ConnectivityManager.TYPE_MOBILE_FOTA:
            return DctConstants.APN_FOTA_ID;
        case ConnectivityManager.TYPE_MOBILE_IMS:
            return DctConstants.APN_IMS_ID;
        case ConnectivityManager.TYPE_MOBILE_CBS:
            return DctConstants.APN_CBS_ID;
        case ConnectivityManager.TYPE_MOBILE_IA:
            return DctConstants.APN_IA_ID;
        case ConnectivityManager.TYPE_MOBILE_EMERGENCY:
            return DctConstants.APN_EMERGENCY_ID;
        default:
            return DctConstants.APN_INVALID_ID;
        }
    }

    public static int apnIdForNetworkRequest(NetworkRequest nr) {
        NetworkCapabilities nc = nr.networkCapabilities;
        // For now, ignore the bandwidth stuff
        if (nc.getTransportTypes().length > 0 &&
                nc.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == false) {
            return DctConstants.APN_INVALID_ID;
        }

        // in the near term just do 1-1 matches.
        // TODO - actually try to match the set of capabilities
        int apnId = DctConstants.APN_INVALID_ID;
        boolean error = false;

        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
            apnId = DctConstants.APN_DEFAULT_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_MMS_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_SUPL_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_DUN_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_FOTA_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_IMS_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_CBS_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_IA)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_IA_ID;
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_RCS)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;

            Rlog.d(SLOG_TAG, "RCS APN type not yet supported");
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_XCAP)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;

            Rlog.d(SLOG_TAG, "XCAP APN type not yet supported");
        }
        if (nc.hasCapability(NetworkCapabilities.NET_CAPABILITY_EIMS)) {
            if (apnId != DctConstants.APN_INVALID_ID) error = true;
            apnId = DctConstants.APN_MMS_ID;
        }
        if (error) {
            // TODO: If this error condition is removed, the framework's handling of
            // NET_CAPABILITY_NOT_RESTRICTED will need to be updated so requests for
            // say FOTA and INTERNET are marked as restricted.  This is not how
            // NetworkCapabilities.maybeMarkCapabilitiesRestricted currently works.
            Rlog.d(SLOG_TAG, "Multiple apn types specified in request - result is unspecified!");
        }
        if (apnId == DctConstants.APN_INVALID_ID) {
            Rlog.d(SLOG_TAG, "Unsupported NetworkRequest in Telephony: nr=" + nr);
        }
        return apnId;
    }

    // TODO - kill The use of these strings
    public static int apnIdForApnName(String type) {
        switch (type) {
            case PhoneConstants.APN_TYPE_DEFAULT:
                return DctConstants.APN_DEFAULT_ID;
            case PhoneConstants.APN_TYPE_MMS:
                return DctConstants.APN_MMS_ID;
            case PhoneConstants.APN_TYPE_SUPL:
                return DctConstants.APN_SUPL_ID;
            case PhoneConstants.APN_TYPE_DUN:
                return DctConstants.APN_DUN_ID;
            case PhoneConstants.APN_TYPE_HIPRI:
                return DctConstants.APN_HIPRI_ID;
            case PhoneConstants.APN_TYPE_IMS:
                return DctConstants.APN_IMS_ID;
            case PhoneConstants.APN_TYPE_FOTA:
                return DctConstants.APN_FOTA_ID;
            case PhoneConstants.APN_TYPE_CBS:
                return DctConstants.APN_CBS_ID;
            case PhoneConstants.APN_TYPE_IA:
                return DctConstants.APN_IA_ID;
            case PhoneConstants.APN_TYPE_EMERGENCY:
                return DctConstants.APN_EMERGENCY_ID;
            default:
                return DctConstants.APN_INVALID_ID;
        }
    }

    private static String apnNameForApnId(int id) {
        switch (id) {
            case DctConstants.APN_DEFAULT_ID:
                return PhoneConstants.APN_TYPE_DEFAULT;
            case DctConstants.APN_MMS_ID:
                return PhoneConstants.APN_TYPE_MMS;
            case DctConstants.APN_SUPL_ID:
                return PhoneConstants.APN_TYPE_SUPL;
            case DctConstants.APN_DUN_ID:
                return PhoneConstants.APN_TYPE_DUN;
            case DctConstants.APN_HIPRI_ID:
                return PhoneConstants.APN_TYPE_HIPRI;
            case DctConstants.APN_IMS_ID:
                return PhoneConstants.APN_TYPE_IMS;
            case DctConstants.APN_FOTA_ID:
                return PhoneConstants.APN_TYPE_FOTA;
            case DctConstants.APN_CBS_ID:
                return PhoneConstants.APN_TYPE_CBS;
            case DctConstants.APN_IA_ID:
                return PhoneConstants.APN_TYPE_IA;
            case DctConstants.APN_EMERGENCY_ID:
                return PhoneConstants.APN_TYPE_EMERGENCY;
            default:
                Rlog.d(SLOG_TAG, "Unknown id (" + id + ") in apnIdToType");
                return PhoneConstants.APN_TYPE_DEFAULT;
        }
    }

    @Override
    public synchronized String toString() {
        // We don't print mDataConnection because its recursive.
        return "{mApnType=" + mApnType + " mState=" + getState() + " mWaitingApns={" +
                mRetryManager.getWaitingApns() + "}" + " mApnSetting={" + mApnSetting +
                "} mReason=" + mReason + " mDataEnabled=" + mDataEnabled + " mDependencyMet=" +
                mDependencyMet + "}";
    }

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

    public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) {
        final IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        synchronized (mRefCountLock) {
            pw.println(toString());
            pw.increaseIndent();
            for (LocalLog l : mLocalLogs) {
                l.dump(fd, pw, args);
            }
            if (mHistoryLogs.size() > 0) pw.println("Historical Logs:");
            for (LocalLog l : mHistoryLogs) {
                l.dump(fd, pw, args);
            }
            pw.decreaseIndent();
            pw.println("mRetryManager={" + mRetryManager.toString() + "}");
        }
    }
}
