/*
 * 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.Context;
import android.content.res.Resources;
import android.net.NetworkConfig;
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.util.IndentingPrintWriter;

import java.io.FileDescriptor;
import java.io.PrintWriter;
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;

    protected static final boolean DBG = false;

    private final Context mContext;

    private final String mApnType;

    private DctConstants.State mState;

    private ArrayList<ApnSetting> mWaitingApns = null;

    /**
     * Used to check if conditions (new RAT) are resulting in a new list which warrants a retry.
     * Set in the last trySetupData call.
     */
    private ArrayList<ApnSetting> mOriginalWaitingApns = null;

    public final int priority;

    /** A zero indicates that all waiting APNs had a permanent error */
    private AtomicInteger mWaitingApnsPermanentFailureCountDown;

    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 DcTrackerBase 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);

    public ApnContext(Context context, String apnType, String logTag, NetworkConfig config,
            DcTrackerBase tracker) {
        mContext = context;
        mApnType = apnType;
        mState = DctConstants.State.IDLE;
        setReason(Phone.REASON_DATA_ENABLED);
        mDataEnabled = new AtomicBoolean(false);
        mDependencyMet = new AtomicBoolean(config.dependencyMet);
        mWaitingApnsPermanentFailureCountDown = new AtomicInteger(0);
        priority = config.priority;
        LOG_TAG = logTag;
        mDcTracker = tracker;
    }

    public String getApnType() {
        return mApnType;
    }

    public synchronized DcAsyncChannel getDcAc() {
        return mDcAc;
    }

    public synchronized void setDataConnectionAc(DcAsyncChannel dcac) {
        if (DBG) {
            log("setDataConnectionAc: old dcac=" + mDcAc + " new dcac=" + dcac
                    + " this=" + this);
        }
        mDcAc = dcac;
    }

    public synchronized void releaseDataConnection(String reason) {
        if (mDcAc != null) {
            mDcAc.tearDown(this, reason, null);
            mDcAc = null;
        }
        setState(DctConstants.State.IDLE);
    }

    public synchronized PendingIntent getReconnectIntent() {
        return mReconnectAlarmIntent;
    }

    public synchronized void setReconnectIntent(PendingIntent intent) {
        mReconnectAlarmIntent = intent;
    }

    public synchronized ApnSetting getApnSetting() {
        if (DBG) log("getApnSetting: apnSetting=" + mApnSetting);
        return mApnSetting;
    }

    public synchronized void setApnSetting(ApnSetting apnSetting) {
        if (DBG) log("setApnSetting: apnSetting=" + apnSetting);
        mApnSetting = apnSetting;
    }

    public synchronized void setWaitingApns(ArrayList<ApnSetting> waitingApns) {
        mWaitingApns = waitingApns;
        mOriginalWaitingApns = new ArrayList<ApnSetting>(waitingApns);
        mWaitingApnsPermanentFailureCountDown.set(mWaitingApns.size());
    }

    public int getWaitingApnsPermFailCount() {
        return mWaitingApnsPermanentFailureCountDown.get();
    }

    public void decWaitingApnsPermFailCount() {
        mWaitingApnsPermanentFailureCountDown.decrementAndGet();
    }

    public synchronized ApnSetting getNextWaitingApn() {
        ArrayList<ApnSetting> list = mWaitingApns;
        ApnSetting apn = null;

        if (list != null) {
            if (!list.isEmpty()) {
                apn = list.get(0);
            }
        }
        return apn;
    }

    public synchronized void removeWaitingApn(ApnSetting apn) {
        if (mWaitingApns != null) {
            mWaitingApns.remove(apn);
        }
    }

    public synchronized ArrayList<ApnSetting> getOriginalWaitingApns() {
        return mOriginalWaitingApns;
    }

    public synchronized ArrayList<ApnSetting> getWaitingApns() {
        return mWaitingApns;
    }

    public synchronized void setConcurrentVoiceAndDataAllowed(boolean allowed) {
        mConcurrentVoiceAndDataAllowed = allowed;
    }

    public synchronized boolean isConcurrentVoiceAndDataAllowed() {
        return mConcurrentVoiceAndDataAllowed;
    }

    public synchronized void setState(DctConstants.State s) {
        if (DBG) {
            log("setState: " + s + ", previous state:" + mState);
        }

        mState = s;

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

    public synchronized DctConstants.State getState() {
        return mState;
    }

    public boolean isDisconnected() {
        DctConstants.State currentState = getState();
        return ((currentState == DctConstants.State.IDLE) ||
                    currentState == DctConstants.State.FAILED);
    }

    public synchronized void setReason(String reason) {
        if (DBG) {
            log("set reason as " + reason + ",current state " + mState);
        }
        mReason = reason;
    }

    public synchronized String getReason() {
        return mReason;
    }

    public boolean isReady() {
        return mDataEnabled.get() && mDependencyMet.get();
    }

    public boolean isConnectable() {
        return isReady() && ((mState == DctConstants.State.IDLE)
                                || (mState == DctConstants.State.SCANNING)
                                || (mState == DctConstants.State.RETRYING)
                                || (mState == DctConstants.State.FAILED));
    }

    public boolean isConnectedOrConnecting() {
        return isReady() && ((mState == DctConstants.State.CONNECTED)
                                || (mState == DctConstants.State.CONNECTING)
                                || (mState == DctConstants.State.SCANNING)
                                || (mState == DctConstants.State.RETRYING));
    }

    public void setEnabled(boolean enabled) {
        if (DBG) {
            log("set enabled as " + enabled + ", current state is " + mDataEnabled.get());
        }
        mDataEnabled.set(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 = mContext.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>();

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

    public void incRefCount(LocalLog log) {
        synchronized (mRefCountLock) {
            if (mRefCount == 0) {
               // we wanted to leave the last in so it could actually capture the tear down
               // of the network
               requestLog("clearing log with size=" + mLocalLogs.size());
               mLocalLogs.clear();
            }
            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(mDcTracker.apnTypeToId(mApnType), true);
            }
        }
    }

    public void decRefCount(LocalLog log) {
        synchronized (mRefCountLock) {
            // leave the last log alive to capture the actual tear down
            if (mRefCount != 1) {
                if (mLocalLogs.remove(log)) {
                    log.log("ApnContext.decRefCount - " + mRefCount);
                } else {
                    log.log("ApnContext.decRefCount didn't find log - " + mRefCount);
                }
            } else {
                log.log("ApnContext.decRefCount - 1");
            }
            if (mRefCount-- == 1) {
                mDcTracker.setEnabled(mDcTracker.apnTypeToId(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 = Resources.getSystem().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();
    }

    @Override
    public synchronized String toString() {
        // We don't print mDataConnection because its recursive.
        return "{mApnType=" + mApnType + " mState=" + getState() + " mWaitingApns={" +
                mWaitingApns + "} mWaitingApnsPermanentFailureCountDown=" +
                mWaitingApnsPermanentFailureCountDown + " 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());
            if (mRefCount > 0) {
                pw.increaseIndent();
                for (LocalLog l : mLocalLogs) {
                    l.dump(fd, pw, args);
                }
                pw.decreaseIndent();
            }
        }
    }
}
