/*
 * Copyright (C) 2015 Jacob Klinker
 *
 * 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.mms.service_alt;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.SSLCertificateSocketFactory;
import android.os.Build;
import android.os.SystemClock;
import com.android.mms.service_alt.exception.MmsNetworkException;
import com.squareup.okhttp.ConnectionPool;
import timber.log.Timber;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class MmsNetworkManager implements com.squareup.okhttp.internal.Network {

    // Timeout used to call ConnectivityManager.requestNetwork
    private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000;
    // Wait timeout for this class, a little bit longer than the above timeout
    // to make sure we don't bail prematurely
    private static final int NETWORK_ACQUIRE_TIMEOUT_MILLIS =
            NETWORK_REQUEST_TIMEOUT_MILLIS + (5 * 1000);

    // Borrowed from {@link android.net.Network}
    private static final boolean httpKeepAlive =
            Boolean.parseBoolean(System.getProperty("http.keepAlive", "true"));
    private static final int httpMaxConnections =
            httpKeepAlive ? Integer.parseInt(System.getProperty("http.maxConnections", "5")) : 0;
    private static final long httpKeepAliveDurationMs =
            Long.parseLong(System.getProperty("http.keepAliveDuration", "300000"));  // 5 minutes.

    private final Context mContext;

    // The requested MMS {@link android.net.Network} we are holding
    // We need this when we unbind from it. This is also used to indicate if the
    // MMS network is available.
    private Network mNetwork;
    // The current count of MMS requests that require the MMS network
    // If mMmsRequestCount is 0, we should release the MMS network.
    private int mMmsRequestCount;
    // This is really just for using the capability
    private NetworkRequest mNetworkRequest;
    // The callback to register when we request MMS network
    private ConnectivityManager.NetworkCallback mNetworkCallback;

    private volatile ConnectivityManager mConnectivityManager;

    // The OkHttp's ConnectionPool used by the HTTP client associated with this network manager
    private ConnectionPool mConnectionPool;

    // The MMS HTTP client for this network
    private MmsHttpClient mMmsHttpClient;

    // The SIM ID which we use to connect
    private final int mSubId;

    private boolean permissionError = false;

    public MmsNetworkManager(Context context, int subId) {
        mContext = context;
        mNetworkCallback = null;
        mNetwork = null;
        mMmsRequestCount = 0;
        mConnectivityManager = null;
        mConnectionPool = null;
        mMmsHttpClient = null;
        mSubId = subId;

        if (!MmsRequest.useWifi(context)) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
                mNetworkRequest = new NetworkRequest.Builder()
                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                        .addCapability(NetworkCapabilities.NET_CAPABILITY_MMS)
                        .setNetworkSpecifier(Integer.toString(mSubId))
                        .build();
            } else {
                mNetworkRequest = new NetworkRequest.Builder()
                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                        .addCapability(NetworkCapabilities.NET_CAPABILITY_MMS)
                        .build();
            }
        } else {
            mNetworkRequest = new NetworkRequest.Builder()
                    .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                    .build();
        }

        MmsConfigManager.getInstance().init(context);
    }

    /**
     * Acquire the MMS network
     *
     * @throws MmsNetworkException if we fail to acquire it
     */
    public Network acquireNetwork() throws MmsNetworkException {
        synchronized (this) {
            mMmsRequestCount += 1;
            if (mNetwork != null) {
                // Already available
                Timber.d("MmsNetworkManager: already available");
                return mNetwork;
            }
            Timber.d("MmsNetworkManager: start new network request");
            // Not available, so start a new request
            newRequest();
            final long shouldEnd = SystemClock.elapsedRealtime() + NETWORK_ACQUIRE_TIMEOUT_MILLIS;
            long waitTime = NETWORK_ACQUIRE_TIMEOUT_MILLIS;
            while (waitTime > 0) {
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    Timber.w("MmsNetworkManager: acquire network wait interrupted");
                }
                if (mNetwork != null || permissionError) {
                    // Success
                    return mNetwork;
                }
                // Calculate remaining waiting time to make sure we wait the full timeout period
                waitTime = shouldEnd - SystemClock.elapsedRealtime();
            }
            // Timed out, so release the request and fail
            Timber.d("MmsNetworkManager: timed out");
            releaseRequestLocked(mNetworkCallback);
            throw new MmsNetworkException("Acquiring network timed out");
        }
    }

    /**
     * Release the MMS network when nobody is holding on to it.
     */
    public void releaseNetwork() {
        synchronized (this) {
            if (mMmsRequestCount > 0) {
                mMmsRequestCount -= 1;
                Timber.d("MmsNetworkManager: release, count=" + mMmsRequestCount);
                if (mMmsRequestCount < 1) {
                    releaseRequestLocked(mNetworkCallback);
                }
            }
        }
    }

    /**
     * Start a new {@link NetworkRequest} for MMS
     */
    private void newRequest() {
        final ConnectivityManager connectivityManager = getConnectivityManager();
        mNetworkCallback = new ConnectivityManager.NetworkCallback() {

            @Override
            public void onAvailable(Network network) {
                super.onAvailable(network);
                Timber.d("NetworkCallbackListener.onAvailable: network=" + network);
                synchronized (MmsNetworkManager.this) {
                    mNetwork = network;
                    MmsNetworkManager.this.notifyAll();
                }
            }

            @Override
            public void onLost(Network network) {
                super.onLost(network);
                Timber.d("NetworkCallbackListener.onLost: network=" + network);
                synchronized (MmsNetworkManager.this) {
                    releaseRequestLocked(this);
                    MmsNetworkManager.this.notifyAll();
                }
            }

//            @Override
//            public void onUnavailable() {
//                super.onUnavailable();
//                Timber.d("NetworkCallbackListener.onUnavailable");
//                synchronized (MmsNetworkManager.this) {
//                    releaseRequestLocked(this);
//                    MmsNetworkManager.this.notifyAll();
//                }
//            }
        };

        try {
            connectivityManager.requestNetwork(
                    mNetworkRequest, mNetworkCallback);
        } catch (SecurityException e) {
            Timber.e(e, "permission exception... skipping it for testing purposes");
            permissionError = true;
        }
    }

    /**
     * Release the current {@link NetworkRequest} for MMS
     *
     * @param callback the {@link ConnectivityManager.NetworkCallback} to unregister
     */
    private void releaseRequestLocked(ConnectivityManager.NetworkCallback callback) {
        if (callback != null) {
            final ConnectivityManager connectivityManager = getConnectivityManager();

            try {
                connectivityManager.unregisterNetworkCallback(callback);
            } catch (Exception e) {
                Timber.e(e, "couldn't unregister");
            }
        }
        resetLocked();
    }

    /**
     * Reset the state
     */
    private void resetLocked() {
        mNetworkCallback = null;
        mNetwork = null;
        mMmsRequestCount = 0;
        // Currently we follow what android.net.Network does with ConnectionPool,
        // which is per Network object. So if Network changes, we should clear
        // out the ConnectionPool and thus the MmsHttpClient (since it is linked
        // to a specific ConnectionPool).
        mConnectionPool = null;
        mMmsHttpClient = null;
    }

    private static final InetAddress[] EMPTY_ADDRESS_ARRAY = new InetAddress[0];

    @Override
    public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
        Network network = null;
        synchronized (this) {
            if (mNetwork == null) {
                return EMPTY_ADDRESS_ARRAY;
            }
            network = mNetwork;
        }
        return network.getAllByName(host);
    }

    private ConnectivityManager getConnectivityManager() {
        if (mConnectivityManager == null) {
            mConnectivityManager = (ConnectivityManager) mContext.getSystemService(
                    Context.CONNECTIVITY_SERVICE);
        }
        return mConnectivityManager;
    }

    private ConnectionPool getOrCreateConnectionPoolLocked() {
        if (mConnectionPool == null) {
            mConnectionPool = new ConnectionPool(httpMaxConnections, httpKeepAliveDurationMs);
        }
        return mConnectionPool;
    }

    /**
     * Get an MmsHttpClient for the current network
     *
     * @return The MmsHttpClient instance
     */
    public MmsHttpClient getOrCreateHttpClient() {
        synchronized (this) {
            if (mMmsHttpClient == null) {
                if (mNetwork != null) {
                    // Create new MmsHttpClient for the current Network
                    mMmsHttpClient = new MmsHttpClient(
                            mContext,
                            mNetwork.getSocketFactory(),
                            MmsNetworkManager.this,
                            getOrCreateConnectionPoolLocked());
                } else if (permissionError) {
                    mMmsHttpClient = new MmsHttpClient(
                            mContext,
                            new SSLCertificateSocketFactory(NETWORK_REQUEST_TIMEOUT_MILLIS),
                            MmsNetworkManager.this,
                            getOrCreateConnectionPoolLocked());
                }
            }
            return mMmsHttpClient;
        }
    }

    /**
     * Get the APN name for the active network
     *
     * @return The APN name if available, otherwise null
     */
    public String getApnName() {
        Network network = null;
        synchronized (this) {
            if (mNetwork == null) {
                Timber.d("MmsNetworkManager: getApnName: network not available");
                mNetworkRequest = new NetworkRequest.Builder()
                        .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                        .build();
                return null;
            }
            network = mNetwork;
        }
        String apnName = null;
        final ConnectivityManager connectivityManager = getConnectivityManager();
        NetworkInfo mmsNetworkInfo = connectivityManager.getNetworkInfo(network);
        if (mmsNetworkInfo != null) {
            apnName = mmsNetworkInfo.getExtraInfo();
        }
        Timber.d("MmsNetworkManager: getApnName: " + apnName);
        return apnName;
    }

}
