/*
 * 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 android.webkit;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.http.*;
import android.os.*;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.Map;

import junit.framework.Assert;

class Network {

    private static final String LOGTAG = "network";

    /**
     * Static instance of a Network object.
     */
    private static Network sNetwork;
    
    /**
     * Flag to store the state of platform notifications, for the case
     * when the Network object has not been constructed yet
     */
    private static boolean sPlatformNotifications;
    
    /**
     * Reference count for platform notifications as the network class is a 
     * static and can exist over multiple activities, thus over multiple 
     * onPause/onResume pairs. 
     */
    private static int sPlatformNotificationEnableRefCount;

    /**
     * Proxy username if known (used for pre-emptive proxy authentication).
     */
    private String mProxyUsername;

    /**
     * Proxy password if known (used for pre-emptive proxy authentication).
     */
    private String mProxyPassword;

    /**
     * Network request queue (requests are added from the browser thread).
     */
    private RequestQueue mRequestQueue;

    /**
     * SSL error handler: takes care of synchronization of multiple async
     * loaders with SSL-related problems.
     */
    private SslErrorHandler mSslErrorHandler;

    /**
     * HTTP authentication handler: takes care of synchronization of HTTP
     * authentication requests.
     */
    private HttpAuthHandler mHttpAuthHandler;

    private Context mContext;

    /**
     * True if the currently used network connection is a roaming phone
     * connection.
     */
    private boolean mRoaming;

    /**
     * Tracks if we are roaming.
     */
    private RoamingMonitor mRoamingMonitor;

    /**
     * @return The singleton instance of the network.
     */
    public static synchronized Network getInstance(Context context) {
        if (sNetwork == null) {
            // Note Context of the Application is used here, rather than
            // the what is passed in (usually a Context derived from an 
            // Activity) so the intent receivers belong to the application
            // rather than an activity - this fixes the issue where 
            // Activities are created and destroyed during the lifetime of
            // an Application
            sNetwork = new Network(context.getApplicationContext());
            if (sPlatformNotifications) {
                // Adjust the ref count before calling enable as it is already
                // taken into account when the static function was called 
                // directly
                --sPlatformNotificationEnableRefCount;
                enablePlatformNotifications();
            }
        }
        return sNetwork;
    }


    /**
     * Enables data state and proxy tracking
     */
    public static void enablePlatformNotifications() {
        if (++sPlatformNotificationEnableRefCount == 1) {
            if (sNetwork != null) {
                sNetwork.mRequestQueue.enablePlatformNotifications();
                sNetwork.monitorRoaming();
            } else {
                sPlatformNotifications = true;
            }
        }
    }

    /**
     * If platform notifications are enabled, this should be called
     * from onPause() or onStop()
     */
    public static void disablePlatformNotifications() {
        if (--sPlatformNotificationEnableRefCount == 0) {
            if (sNetwork != null) {
                sNetwork.mRequestQueue.disablePlatformNotifications();
                sNetwork.stopMonitoringRoaming();
            } else {
                sPlatformNotifications = false;
            }
        }
    }

    /**
     * Creates a new Network object.
     * XXX: Must be created in the same thread as WebCore!!!!!
     */
    private Network(Context context) {
        if (DebugFlags.NETWORK) {
            Assert.assertTrue(Thread.currentThread().
                    getName().equals(WebViewCore.THREAD_NAME));
        }
        mContext = context;
        mSslErrorHandler = new SslErrorHandler();
        mHttpAuthHandler = new HttpAuthHandler(this);

        mRequestQueue = new RequestQueue(context);
    }

    private class RoamingMonitor extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (!ConnectivityManager.CONNECTIVITY_ACTION.equals(intent.getAction()))
                return;

            NetworkInfo info = (NetworkInfo)intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
            if (info != null)
                mRoaming = info.isRoaming();
        };
    };

    private void monitorRoaming() {
        mRoamingMonitor = new RoamingMonitor();
        IntentFilter filter = new IntentFilter();
        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
        mContext.registerReceiver(sNetwork.mRoamingMonitor, filter);
    }

    private void stopMonitoringRoaming() {
        if (mRoamingMonitor != null) {
            mContext.unregisterReceiver(mRoamingMonitor);
            mRoamingMonitor = null;
        }
    }

    /**
     * Request a url from either the network or the file system.
     * @param url The url to load.
     * @param method The http method.
     * @param headers The http headers.
     * @param postData The body of the request.
     * @param loader A LoadListener for receiving the results of the request.
     * @return True if the request was successfully queued.
     */
    public boolean requestURL(String method,
                              Map<String, String> headers,
                              byte [] postData,
                              LoadListener loader) {

        String url = loader.url();

        // Not a valid url, return false because we won't service the request!
        if (!URLUtil.isValidUrl(url)) {
            return false;
        }

        // asset, res, file system or data stream are handled in the other code
        // path. This only handles network request.
        if (URLUtil.isAssetUrl(url) || URLUtil.isResourceUrl(url)
                || URLUtil.isFileUrl(url) || URLUtil.isDataUrl(url)) {
            return false;
        }

        // If this is a prefetch, abort it if we're roaming.
        if (mRoaming && headers.containsKey("X-Moz") && "prefetch".equals(headers.get("X-Moz"))) {
            return false;
        }

        /* FIXME: this is lame.  Pass an InputStream in, rather than
           making this lame one here */
        InputStream bodyProvider = null;
        int bodyLength = 0;
        if (postData != null) {
            bodyLength = postData.length;
            bodyProvider = new ByteArrayInputStream(postData);
        }

        RequestQueue q = mRequestQueue;
        RequestHandle handle = null;
        if (loader.isSynchronous()) {
            handle = q.queueSynchronousRequest(url, loader.getWebAddress(),
                    method, headers, loader, bodyProvider, bodyLength);
            loader.attachRequestHandle(handle);
            handle.processRequest();
            loader.loadSynchronousMessages();
        } else {
            handle = q.queueRequest(url, loader.getWebAddress(), method,
                    headers, loader, bodyProvider, bodyLength);
            // FIXME: Although this is probably a rare condition, normal network
            // requests are processed in a separate thread. This means that it
            // is possible to process part of the request before setting the
            // request handle on the loader. We should probably refactor this to
            // ensure the handle is attached before processing begins.
            loader.attachRequestHandle(handle);
        }

        return true;
    }

    /**
     * @return True iff there is a valid proxy set.
     */
    public boolean isValidProxySet() {
        // The proxy host and port can be set within a different thread during
        // an Intent broadcast.
        synchronized (mRequestQueue) {
            return mRequestQueue.getProxyHost() != null;
        }
    }

    /**
     * Get the proxy hostname.
     * @return The proxy hostname obtained from the network queue and proxy
     *         settings.
     */
    public String getProxyHostname() {
        return mRequestQueue.getProxyHost().getHostName();
    }

    /**
     * @return The proxy username or null if none.
     */
    public synchronized String getProxyUsername() {
        return mProxyUsername;
    }

    /**
     * Sets the proxy username.
     * @param proxyUsername Username to use when
     * connecting through the proxy.
     */
    public synchronized void setProxyUsername(String proxyUsername) {
        if (DebugFlags.NETWORK) {
            Assert.assertTrue(isValidProxySet());
        }

        mProxyUsername = proxyUsername;
    }

    /**
     * @return The proxy password or null if none.
     */
    public synchronized String getProxyPassword() {
        return mProxyPassword;
    }

    /**
     * Sets the proxy password.
     * @param proxyPassword Password to use when
     * connecting through the proxy.
     */
    public synchronized void setProxyPassword(String proxyPassword) {
        if (DebugFlags.NETWORK) {
            Assert.assertTrue(isValidProxySet());
        }

        mProxyPassword = proxyPassword;
    }

    /**
     * Saves the state of network handlers (user SSL and HTTP-authentication
     * preferences).
     * @param outState The out-state to save (write) to.
     * @return True iff succeeds.
     */
    public boolean saveState(Bundle outState) {
        if (DebugFlags.NETWORK) {
            Log.v(LOGTAG, "Network.saveState()");
        }

        return mSslErrorHandler.saveState(outState);
    }

    /**
     * Restores the state of network handlers (user SSL and HTTP-authentication
     * preferences).
     * @param inState The in-state to load (read) from.
     * @return True iff succeeds.
     */
    public boolean restoreState(Bundle inState) {
        if (DebugFlags.NETWORK) {
            Log.v(LOGTAG, "Network.restoreState()");
        }

        return mSslErrorHandler.restoreState(inState);
    }

    /**
     * Clears user SSL-error preference table.
     */
    public void clearUserSslPrefTable() {
        mSslErrorHandler.clear();
    }

    /**
     * Handles SSL error(s) on the way up to the user: the user must decide
     * whether errors should be ignored or not.
     * @param loader The loader that resulted in SSL errors.
     */
    public void handleSslErrorRequest(LoadListener loader) {
        if (DebugFlags.NETWORK) Assert.assertNotNull(loader);
        if (loader != null) {
            mSslErrorHandler.handleSslErrorRequest(loader);
        }
    }

    /* package */ boolean checkSslPrefTable(LoadListener loader,
            SslError error) {
        if (loader != null && error != null) {
            return mSslErrorHandler.checkSslPrefTable(loader, error);
        }
        return false;
    }

     /**
     * Handles authentication requests on their way up to the user (the user
     * must provide credentials).
     * @param loader The loader that resulted in an HTTP
     * authentication request.
     */
    public void handleAuthRequest(LoadListener loader) {
        if (DebugFlags.NETWORK) Assert.assertNotNull(loader);
        if (loader != null) {
            mHttpAuthHandler.handleAuthRequest(loader);
        }
    }

    // Performance probe
    public void startTiming() {
        mRequestQueue.startTiming();
    }

    public void stopTiming() {
        mRequestQueue.stopTiming();
    }
}
